refactor: server - cursor pagination (#56)

Reviewed-on: twhelp/corev3#56
This commit is contained in:
Dawid Wysokiński 2024-02-07 07:17:47 +00:00
parent b6d55b1741
commit f07451351b
17 changed files with 600 additions and 280 deletions

View File

@ -64,18 +64,25 @@ func (repo *ServerBunRepository) CreateOrUpdate(ctx context.Context, params ...d
return nil
}
func (repo *ServerBunRepository) List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) {
func (repo *ServerBunRepository) List(
ctx context.Context,
params domain.ListServersParams,
) (domain.ListServersResult, error) {
var servers bunmodel.Servers
if err := repo.baseListQuery(params).Model(&servers).Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select servers from the db: %w", err)
if err := repo.db.NewSelect().
Model(&servers).
Apply(listServersParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.ListServersResult{}, fmt.Errorf("couldn't select servers from the db: %w", err)
}
return servers.ToDomain()
}
converted, err := servers.ToDomain()
if err != nil {
return domain.ListServersResult{}, err
}
func (repo *ServerBunRepository) baseListQuery(params domain.ListServersParams) *bun.SelectQuery {
return repo.db.NewSelect().Apply(listServersParamsApplier{params: params}.apply)
return domain.NewListServersResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *ServerBunRepository) Update(ctx context.Context, key string, params domain.UpdateServerParams) error {
@ -184,10 +191,6 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("server.key IN (?)", bun.In(keys))
}
if keyGT := a.params.KeyGT(); keyGT.Valid {
q = q.Where("server.key > ?", keyGT.Value)
}
if versionCodes := a.params.VersionCodes(); len(versionCodes) > 0 {
q = q.Where("server.version_code IN (?)", bun.In(versionCodes))
}
@ -229,5 +232,29 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
}
}
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
if !a.params.Cursor().IsZero() {
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
cursorKey := a.params.Cursor().Key()
cursorOpen := a.params.Cursor().Open()
for _, s := range a.params.Sort() {
switch s {
case domain.ServerSortKeyASC:
q = q.Where("server.key >= ?", cursorKey)
case domain.ServerSortKeyDESC:
q = q.Where("server.key <= ?", cursorKey)
case domain.ServerSortOpenASC:
q = q.Where("server.open >= ?", cursorOpen)
case domain.ServerSortOpenDESC:
q = q.Where("server.open <= ?", cursorOpen)
default:
return q.Err(errors.New("unsupported sort value"))
}
}
return q
})
}
return q.Limit(a.params.Limit() + 1)
}

View File

@ -89,10 +89,10 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Run("OK", func(t *testing.T) {
t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams())
listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err)
require.NotEmpty(t, servers)
server := servers[0]
require.NotEmpty(t, listServersRes)
server := listServersRes.Servers()[0]
ennoblementsToCreate := domain.BaseEnnoblements{
domaintest.NewBaseEnnoblement(t),

View File

@ -64,10 +64,10 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Run("OK", func(t *testing.T) {
t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams())
listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err)
require.NotEmpty(t, servers)
server := servers[0]
require.NotEmpty(t, listServersRes)
server := listServersRes.Servers()[0]
playersToCreate := domain.BasePlayers{
domaintest.NewBasePlayer(t),
@ -325,15 +325,15 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
servers, listServersErr := repos.server.List(ctx, listServersParams)
listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr)
require.NotEmpty(t, servers)
require.NotEmpty(t, listServersRes)
t.Run("OK", func(t *testing.T) {
t.Parallel()
serverKeys := make([]string, 0, len(servers))
for _, s := range servers {
serverKeys := make([]string, 0, len(listServersRes.Servers()))
for _, s := range listServersRes.Servers() {
serverKeys = append(serverKeys, s.Key())
}
@ -386,7 +386,7 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Run("OK: len(ids) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.player.Delete(ctx, servers[0].Key()))
require.NoError(t, repos.player.Delete(ctx, listServersRes.Servers()[0].Key()))
})
})
}

View File

@ -3,7 +3,6 @@ package adapter_test
import (
"cmp"
"context"
"fmt"
"math"
"slices"
"testing"
@ -39,8 +38,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
listParams := domain.NewListServersParams()
require.NoError(t, listParams.SetKeys(keys))
servers, err := repos.server.List(ctx, listParams)
res, err := repos.server.List(ctx, listParams)
require.NoError(t, err)
servers := res.Servers()
require.Len(t, servers, len(params))
for i, p := range params {
idx := slices.IndexFunc(servers, func(server domain.Server) bool {
@ -99,11 +99,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
repos := newRepos(t)
servers, listServersErr := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, listServersErr)
require.NotEmpty(t, servers)
randServer := servers[0]
snapshotsCreatedAtLT := time.Date(
2022,
time.March,
@ -116,11 +111,10 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
)
tests := []struct {
name string
params func(t *testing.T) domain.ListServersParams
assertServers func(t *testing.T, params domain.ListServersParams, servers domain.Servers)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListServersParams, total int)
name string
params func(t *testing.T) domain.ListServersParams
assertResult func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
@ -128,8 +122,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
return domain.NewListServersParams()
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
return cmp.Compare(a.Key(), b.Key())
@ -139,10 +134,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[open ASC, key ASC]",
@ -152,8 +143,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenASC, domain.ServerSortKeyASC}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
@ -171,10 +163,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[open DESC, key DESC]",
@ -184,8 +172,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortKeyDESC}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
@ -203,22 +192,26 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: keys=[%s]", randServer.Key()),
name: "OK: keys",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetKeys([]string{randServer.Key()}))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Servers()))
require.NoError(t, params.SetKeys([]string{res.Servers()[0].Key()}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
keys := params.Keys()
assert.Len(t, servers, len(keys))
@ -232,22 +225,26 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.Equal(t, len(params.Keys()), total) //nolint:testifylint
},
},
{
name: fmt.Sprintf("OK: versionCodes=[%s]", randServer.VersionCode()),
name: "OK: versionCodes",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetVersionCodes([]string{randServer.VersionCode()}))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Servers()))
require.NoError(t, params.SetVersionCodes([]string{res.Servers()[0].VersionCode()}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
versionCodes := params.VersionCodes()
assert.NotEmpty(t, servers)
@ -261,37 +258,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.Equal(t, len(params.Keys()), total) //nolint:testifylint
},
},
{
name: fmt.Sprintf("OK: keyGT=%s", randServer.Key()),
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetKeyGT(domain.NullString{
Value: randServer.Key(),
Valid: true,
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
t.Helper()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.Greater(t, s.Key(), params.KeyGT().Value, s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: special=true",
@ -304,8 +270,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.True(t, s.Special(), s.Key())
@ -315,10 +282,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: open=false",
@ -331,8 +294,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.False(t, s.Open(), s.Key())
@ -342,10 +306,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: playerSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
@ -358,8 +318,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.True(t, s.PlayerSnapshotsCreatedAt().Before(snapshotsCreatedAtLT))
@ -369,10 +330,6 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: tribeSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
@ -385,8 +342,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
}))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
for _, s := range servers {
assert.True(t, s.TribeSnapshotsCreatedAt().Before(snapshotsCreatedAtLT))
@ -396,31 +354,108 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: offset=1 limit=2",
name: "OK: cursor sort=[open ASC, key ASC]",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenASC, domain.ServerSortKeyASC}))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Servers()), 2)
require.NoError(t, params.SetCursor(domaintest.NewServerCursor(t, func(cfg *domaintest.ServerCursorConfig) {
cfg.Key = res.Servers()[1].Key()
cfg.Open = res.Servers()[1].Open()
})))
return params
},
assertServers: func(t *testing.T, params domain.ListServersParams, servers domain.Servers) {
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
assert.Len(t, servers, params.Limit())
servers := res.Servers()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
return 1
}
if !a.Open() && b.Open() {
return -1
}
return cmp.Compare(a.Key(), b.Key())
}))
for _, s := range res.Servers() {
assert.GreaterOrEqual(t, s.Key(), params.Cursor().Key(), s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListServersParams, total int) {
},
{
name: "OK: cursor sort=[open DESC, key DESC]",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListServersParams()
require.NoError(t, params.SetSort([]domain.ServerSort{domain.ServerSortOpenDESC, domain.ServerSortKeyDESC}))
res, err := repos.server.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Servers()), 2)
require.NoError(t, params.SetCursor(domaintest.NewServerCursor(t, func(cfg *domaintest.ServerCursorConfig) {
cfg.Key = res.Servers()[1].Key()
cfg.Open = res.Servers()[1].Open()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, len(servers))
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
if a.Open() && !b.Open() {
return -1
}
if !a.Open() && b.Open() {
return 1
}
return cmp.Compare(a.Key(), b.Key()) * -1
}))
for _, s := range res.Servers() {
assert.LessOrEqual(t, s.Key(), params.Cursor().Key(), s.Key())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(t *testing.T, params domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
assert.Len(t, res.Servers(), params.Limit())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
}
@ -435,7 +470,7 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
res, err := repos.server.List(ctx, params)
tt.assertError(t, err)
tt.assertServers(t, params, res)
tt.assertResult(t, params, res)
})
}
})
@ -454,6 +489,7 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
serversBeforeUpdate, err := repos.server.List(ctx, listServersBeforeUpdateParams)
require.NoError(t, err)
require.NotEmpty(t, serversBeforeUpdate)
serverBeforeUpdate := serversBeforeUpdate.Servers()[0]
var updateParams domain.UpdateServerParams
require.NoError(t, updateParams.SetConfig(domain.NullServerConfig{
@ -517,58 +553,59 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
Valid: true,
}))
require.NoError(t, repos.server.Update(ctx, serversBeforeUpdate[0].Key(), updateParams))
require.NoError(t, repos.server.Update(ctx, serverBeforeUpdate.Key(), updateParams))
listServersAfterUpdateParams := domain.NewListServersParams()
require.NoError(t, listServersAfterUpdateParams.SetLimit(1))
require.NoError(t, listServersAfterUpdateParams.SetKeys([]string{serversBeforeUpdate[0].Key()}))
require.NoError(t, listServersAfterUpdateParams.SetKeys([]string{serverBeforeUpdate.Key()}))
serversAfterUpdate, err := repos.server.List(ctx, listServersAfterUpdateParams)
require.NoError(t, err)
require.NotEmpty(t, serversAfterUpdate)
assert.Equal(t, updateParams.Config().Value, serversAfterUpdate[0].Config())
assert.Equal(t, updateParams.UnitInfo().Value, serversAfterUpdate[0].UnitInfo())
assert.Equal(t, updateParams.BuildingInfo().Value, serversAfterUpdate[0].BuildingInfo())
assert.Equal(t, updateParams.NumTribes().Value, serversAfterUpdate[0].NumTribes())
serverAfterUpdate := serversAfterUpdate.Servers()[0]
assert.Equal(t, updateParams.Config().Value, serverAfterUpdate.Config())
assert.Equal(t, updateParams.UnitInfo().Value, serverAfterUpdate.UnitInfo())
assert.Equal(t, updateParams.BuildingInfo().Value, serverAfterUpdate.BuildingInfo())
assert.Equal(t, updateParams.NumTribes().Value, serverAfterUpdate.NumTribes())
assert.WithinDuration(
t,
updateParams.TribeDataSyncedAt().Value,
serversAfterUpdate[0].TribeDataSyncedAt(),
serverAfterUpdate.TribeDataSyncedAt(),
time.Minute,
)
assert.Equal(t, updateParams.NumPlayers().Value, serversAfterUpdate[0].NumPlayers())
assert.Equal(t, updateParams.NumPlayers().Value, serverAfterUpdate.NumPlayers())
assert.WithinDuration(
t,
updateParams.PlayerDataSyncedAt().Value,
serversAfterUpdate[0].PlayerDataSyncedAt(),
serverAfterUpdate.PlayerDataSyncedAt(),
time.Minute,
)
assert.Equal(t, updateParams.NumVillages().Value, serversAfterUpdate[0].NumVillages())
assert.Equal(t, updateParams.NumPlayerVillages().Value, serversAfterUpdate[0].NumPlayerVillages())
assert.Equal(t, updateParams.NumBarbarianVillages().Value, serversAfterUpdate[0].NumBarbarianVillages())
assert.Equal(t, updateParams.NumBonusVillages().Value, serversAfterUpdate[0].NumBonusVillages())
assert.Equal(t, updateParams.NumVillages().Value, serverAfterUpdate.NumVillages())
assert.Equal(t, updateParams.NumPlayerVillages().Value, serverAfterUpdate.NumPlayerVillages())
assert.Equal(t, updateParams.NumBarbarianVillages().Value, serverAfterUpdate.NumBarbarianVillages())
assert.Equal(t, updateParams.NumBonusVillages().Value, serverAfterUpdate.NumBonusVillages())
assert.WithinDuration(
t,
updateParams.VillageDataSyncedAt().Value,
serversAfterUpdate[0].VillageDataSyncedAt(),
serverAfterUpdate.VillageDataSyncedAt(),
time.Minute,
)
assert.WithinDuration(
t,
updateParams.EnnoblementDataSyncedAt().Value,
serversAfterUpdate[0].EnnoblementDataSyncedAt(),
serverAfterUpdate.EnnoblementDataSyncedAt(),
time.Minute,
)
assert.WithinDuration(
t,
updateParams.TribeSnapshotsCreatedAt().Value,
serversAfterUpdate[0].TribeSnapshotsCreatedAt(),
serverAfterUpdate.TribeSnapshotsCreatedAt(),
time.Minute,
)
assert.WithinDuration(
t,
updateParams.PlayerSnapshotsCreatedAt().Value,
serversAfterUpdate[0].PlayerSnapshotsCreatedAt(),
serverAfterUpdate.PlayerSnapshotsCreatedAt(),
time.Minute,
)
})

View File

@ -17,7 +17,7 @@ type versionRepository interface {
type serverRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error
List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error)
List(ctx context.Context, params domain.ListServersParams) (domain.ListServersResult, error)
Update(ctx context.Context, key string, params domain.UpdateServerParams) error
}

View File

@ -63,10 +63,10 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Run("OK", func(t *testing.T) {
t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams())
listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err)
require.NotEmpty(t, servers)
server := servers[0]
require.NotEmpty(t, listServersRes)
server := listServersRes.Servers()[0]
tribesToCreate := domain.BaseTribes{
domaintest.NewBaseTribe(t),
@ -114,9 +114,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
Valid: true,
}))
servers, listServersErr := repos.server.List(ctx, listServersParams)
listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr)
require.GreaterOrEqual(t, len(servers), 2)
require.GreaterOrEqual(t, len(listServersRes.Servers()), 2)
tests := []struct {
name string
@ -125,12 +125,12 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}{
{
name: "numPlayerVillages=0",
serverKey: servers[0].Key(),
serverKey: listServersRes.Servers()[0].Key(),
numPlayerVillages: 0,
},
{
name: "numPlayerVillages=35000",
serverKey: servers[1].Key(),
serverKey: listServersRes.Servers()[1].Key(),
numPlayerVillages: 35000,
},
}
@ -390,15 +390,15 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
servers, listServersErr := repos.server.List(ctx, listServersParams)
listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr)
require.NotEmpty(t, servers)
require.NotEmpty(t, listServersRes)
t.Run("OK", func(t *testing.T) {
t.Parallel()
serverKeys := make([]string, 0, len(servers))
for _, s := range servers {
serverKeys := make([]string, 0, len(listServersRes.Servers()))
for _, s := range listServersRes.Servers() {
serverKeys = append(serverKeys, s.Key())
}
@ -450,7 +450,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Run("OK: len(ids) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.tribe.Delete(ctx, servers[0].Key()))
require.NoError(t, repos.tribe.Delete(ctx, listServersRes.Servers()[0].Key()))
})
})
}

View File

@ -56,10 +56,10 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Run("OK", func(t *testing.T) {
t.Parallel()
servers, err := repos.server.List(ctx, domain.NewListServersParams())
listServersRes, err := repos.server.List(ctx, domain.NewListServersParams())
require.NoError(t, err)
require.NotEmpty(t, servers)
server := servers[0]
require.NotEmpty(t, listServersRes)
server := listServersRes.Servers()[0]
villagesToCreate := domain.BaseVillages{
domaintest.NewBaseVillage(t),
@ -263,15 +263,15 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetSpecial(domain.NullBool{Value: false, Valid: true}))
servers, listServersErr := repos.server.List(ctx, listServersParams)
listServersRes, listServersErr := repos.server.List(ctx, listServersParams)
require.NoError(t, listServersErr)
require.NotEmpty(t, servers)
require.NotEmpty(t, listServersRes)
t.Run("OK", func(t *testing.T) {
t.Parallel()
serverKeys := make([]string, 0, len(servers))
for _, s := range servers {
serverKeys := make([]string, 0, len(listServersRes.Servers()))
for _, s := range listServersRes.Servers() {
serverKeys = append(serverKeys, s.Key())
}
@ -311,7 +311,7 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
t.Run("OK: len(ids) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.village.Delete(ctx, servers[0].Key()))
require.NoError(t, repos.village.Delete(ctx, listServersRes.Servers()[0].Key()))
})
})
}

View File

@ -10,7 +10,7 @@ import (
type ServerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateServerParams) error
List(ctx context.Context, params domain.ListServersParams) (domain.Servers, error)
List(ctx context.Context, params domain.ListServersParams) (domain.ListServersResult, error)
Update(ctx context.Context, key string, params domain.UpdateServerParams) error
}
@ -101,11 +101,8 @@ func (svc *ServerService) ListAllOpen(ctx context.Context, versionCode string) (
}
// ListAll retrieves all servers from the database based on the given params in an optimal way.
// You can't specify a custom limit/offset/sort/keyGT for this operation.
// You can't specify a custom limit/cursor/sort for this operation.
func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServersParams) (domain.Servers, error) {
if err := params.SetOffset(0); err != nil {
return nil, err
}
if err := params.SetLimit(domain.ServerListMaxLimit); err != nil {
return nil, err
}
@ -116,21 +113,18 @@ func (svc *ServerService) ListAll(ctx context.Context, params domain.ListServers
var servers domain.Servers
for {
ss, err := svc.repo.List(ctx, params)
res, err := svc.repo.List(ctx, params)
if err != nil {
return nil, err
}
if len(ss) == 0 {
servers = append(servers, res.Servers()...)
if res.Next().IsZero() {
return servers, nil
}
servers = append(servers, ss...)
if err = params.SetKeyGT(domain.NullString{
Value: ss[len(ss)-1].Key(),
Valid: true,
}); err != nil {
if err = params.SetCursor(res.Next()); err != nil {
return nil, err
}
}

View File

@ -13,6 +13,29 @@ func RandServerKey() string {
return gofakeit.LetterN(5)
}
type ServerCursorConfig struct {
Key string
Open bool
}
func NewServerCursor(tb TestingTB, opts ...func(cfg *ServerCursorConfig)) domain.ServerCursor {
tb.Helper()
cfg := &ServerCursorConfig{
Key: RandServerKey(),
Open: gofakeit.Bool(),
}
for _, opt := range opts {
opt(cfg)
}
vc, err := domain.NewServerCursor(cfg.Key, cfg.Open)
require.NoError(tb, err)
return vc
}
type ServerConfig struct {
Key string
VersionCode string

View File

@ -5,6 +5,7 @@ import (
"math"
"net/url"
"slices"
"strconv"
"time"
)
@ -200,6 +201,10 @@ func (s Server) EnnoblementDataSyncedAt() time.Time {
return s.ennoblementDataSyncedAt
}
func (s Server) IsZero() bool {
return s == Server{}
}
func (s Server) Base() BaseServer {
return BaseServer{
key: s.key,
@ -464,17 +469,77 @@ const (
ServerSortOpenDESC
)
type ServerCursor struct {
key string
open bool
}
const serverCursorModelName = "ServerCursor"
func NewServerCursor(key string, open bool) (ServerCursor, error) {
if err := validateServerKey(key); err != nil {
return ServerCursor{}, ValidationError{
Model: serverCursorModelName,
Field: "key",
Err: err,
}
}
return ServerCursor{key: key, open: open}, nil
}
func decodeServerCursor(encoded string) (ServerCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return ServerCursor{}, err
}
open, err := strconv.ParseBool(m["open"])
if err != nil {
return ServerCursor{}, ErrInvalidCursor
}
vc, err := NewServerCursor(m["key"], open)
if err != nil {
return ServerCursor{}, ErrInvalidCursor
}
return vc, nil
}
func (sc ServerCursor) Key() string {
return sc.key
}
func (sc ServerCursor) Open() bool {
return sc.open
}
func (sc ServerCursor) IsZero() bool {
return sc == ServerCursor{}
}
func (sc ServerCursor) Encode() string {
if sc.IsZero() {
return ""
}
return encodeCursor([][2]string{
{"key", sc.key},
{"open", strconv.FormatBool(sc.open)},
})
}
type ListServersParams struct {
keys []string
keyGT NullString
versionCodes []string
open NullBool
special NullBool
tribeSnapshotsCreatedAtLT NullTime
playerSnapshotsCreatedAtLT NullTime
sort []ServerSort
cursor ServerCursor
limit int
offset int
}
const (
@ -502,15 +567,6 @@ func (params *ListServersParams) SetKeys(keys []string) error {
return nil
}
func (params *ListServersParams) KeyGT() NullString {
return params.keyGT
}
func (params *ListServersParams) SetKeyGT(keyGT NullString) error {
params.keyGT = keyGT
return nil
}
func (params *ListServersParams) VersionCodes() []string {
return params.versionCodes
}
@ -579,6 +635,30 @@ func (params *ListServersParams) SetSort(sort []ServerSort) error {
return nil
}
func (params *ListServersParams) Cursor() ServerCursor {
return params.cursor
}
func (params *ListServersParams) SetCursor(cursor ServerCursor) error {
params.cursor = cursor
return nil
}
func (params *ListServersParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeServerCursor(encoded)
if err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListServersParams) Limit() int {
return params.limit
}
@ -597,22 +677,55 @@ func (params *ListServersParams) SetLimit(limit int) error {
return nil
}
func (params *ListServersParams) Offset() int {
return params.offset
type ListServersResult struct {
servers Servers
self ServerCursor
next ServerCursor
}
func (params *ListServersParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listServersParamsModelName,
Field: "offset",
Err: err,
const listServersResultModelName = "ListServersResult"
func NewListServersResult(servers Servers, next Server) (ListServersResult, error) {
var err error
res := ListServersResult{
servers: servers,
}
if len(servers) > 0 {
res.self, err = NewServerCursor(servers[0].Key(), servers[0].Open())
if err != nil {
return ListServersResult{}, ValidationError{
Model: listServersResultModelName,
Field: "self",
Err: err,
}
}
}
params.offset = offset
if !next.IsZero() {
res.next, err = NewServerCursor(next.Key(), next.Open())
if err != nil {
return ListServersResult{}, ValidationError{
Model: listServersResultModelName,
Field: "next",
Err: err,
}
}
}
return nil
return res, nil
}
func (res ListServersResult) Servers() Servers {
return res.servers
}
func (res ListServersResult) Self() ServerCursor {
return res.self
}
func (res ListServersResult) Next() ServerCursor {
return res.next
}
type ServerNotFoundError struct {

View File

@ -187,6 +187,65 @@ func TestUpdateServerParams_SetNumTribes(t *testing.T) {
}
}
func TestNewServerCursor(t *testing.T) {
t.Parallel()
validServerCursor := domaintest.NewServerCursor(t)
type args struct {
key string
open bool
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
key: validServerCursor.Key(),
open: validServerCursor.Open(),
},
expectedErr: nil,
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
key: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "ServerCursor",
Field: "key",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
sc, err := domain.NewServerCursor(tt.args.key, tt.args.open)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.key, sc.Key())
assert.Equal(t, tt.args.open, sc.Open())
assert.NotEmpty(t, sc.Encode())
})
}
}
func TestListServersParams_SetSort(t *testing.T) {
t.Parallel()
@ -260,6 +319,90 @@ func TestListServersParams_SetSort(t *testing.T) {
}
}
func TestListServersParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewServerCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.ServerCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
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: "ListServersParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListServersParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServersParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedCursor.Key(), params.Cursor().Key())
assert.Equal(t, tt.expectedCursor.Open(), params.Cursor().Open())
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListServersParams_SetLimit(t *testing.T) {
t.Parallel()
@ -325,55 +468,48 @@ func TestListServersParams_SetLimit(t *testing.T) {
}
}
func TestListServersParams_SetOffset(t *testing.T) {
func TestNewListServersResult(t *testing.T) {
t.Parallel()
type args struct {
offset int
servers := domain.Servers{
domaintest.NewServer(t),
domaintest.NewServer(t),
domaintest.NewServer(t),
}
next := domaintest.NewServer(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: "ListServersParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
for _, tt := range tests {
tt := tt
res, err := domain.NewListServersResult(servers, next)
require.NoError(t, err)
assert.Equal(t, servers, res.Servers())
assert.Equal(t, servers[0].Key(), res.Self().Key())
assert.Equal(t, servers[0].Open(), res.Self().Open())
assert.Equal(t, next.Key(), res.Next().Key())
assert.Equal(t, next.Open(), res.Next().Open())
})
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
params := domain.NewListServersParams()
res, err := domain.NewListServersResult(servers, domain.Server{})
require.NoError(t, err)
assert.Equal(t, servers, res.Servers())
assert.Equal(t, servers[0].Key(), res.Self().Key())
assert.Equal(t, servers[0].Open(), res.Self().Open())
assert.True(t, res.Next().IsZero())
})
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.offset, params.Offset())
})
}
t.Run("OK: 0 versions", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServersResult(nil, domain.Server{})
require.NoError(t, err)
assert.Zero(t, res.Servers())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}
type serverKeyValidationTest struct {

View File

@ -23,33 +23,42 @@ const (
cursorKeyValueSeparator = "="
)
func encodeCursor(m map[string]string) string {
if len(m) == 0 {
func encodeCursor(s [][2]string) string {
if len(s) == 0 {
return ""
}
n := len(cursorSeparator) * (len(m) - 1)
for k, v := range m {
n += len(k) + len(v) + len(cursorKeyValueSeparator)
n := len(cursorSeparator) * (len(s) - 1)
for _, el := range s {
n += len(el[0]) + len(el[1]) + len(cursorKeyValueSeparator)
}
var b bytes.Buffer
b.Grow(n)
for k, v := range m {
for _, el := range s {
if b.Len() > 0 {
_, _ = b.WriteString(cursorSeparator)
}
_, _ = b.WriteString(k)
_, _ = b.WriteString(el[0])
_, _ = b.WriteString(cursorKeyValueSeparator)
_, _ = b.WriteString(v)
_, _ = b.WriteString(el[1])
}
return base64.StdEncoding.EncodeToString(b.Bytes())
}
const (
encodedCursorMinLength = 1
encodedCursorMaxLength = 1000
)
func decodeCursor(s string) (map[string]string, error) {
if err := validateStringLen(s, encodedCursorMinLength, encodedCursorMaxLength); err != nil {
return nil, err
}
decodedBytes, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, ErrInvalidCursor

View File

@ -125,16 +125,7 @@ func NewVersionCursor(code string) (VersionCursor, error) {
}, nil
}
const (
encodedVersionCursorMinLength = 1
encodedVersionCursorMaxLength = 1000
)
func decodeVersionCursor(encoded string) (VersionCursor, error) {
if err := validateStringLen(encoded, encodedVersionCursorMinLength, encodedVersionCursorMaxLength); err != nil {
return VersionCursor{}, err
}
m, err := decodeCursor(encoded)
if err != nil {
return VersionCursor{}, err
@ -161,8 +152,8 @@ func (vc VersionCursor) Encode() string {
return ""
}
return encodeCursor(map[string]string{
"code": vc.code,
return encodeCursor([][2]string{
{"code", vc.code},
})
}

View File

@ -214,7 +214,6 @@ func TestListVersionsParams_SetEncodedCursor(t *testing.T) {
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
fmt.Println(tt.expectedErr.Error())
return
}
assert.Equal(t, tt.expectedCursor.Code(), params.Cursor().Code())

View File

@ -215,19 +215,16 @@ func TestDataSync(t *testing.T) {
allServers := make(domain.Servers, 0, len(expectedServers))
for {
servers, err := serverRepo.List(ctx, listParams)
res, err := serverRepo.List(ctx, listParams)
require.NoError(collect, err)
if len(servers) == 0 {
allServers = append(allServers, res.Servers()...)
if res.Next().IsZero() {
break
}
allServers = append(allServers, servers...)
require.NoError(collect, listParams.SetKeyGT(domain.NullString{
Value: servers[len(servers)-1].Key(),
Valid: true,
}))
require.NoError(collect, listParams.SetCursor(res.Next()))
}
if !assert.Len(collect, allServers, len(expectedServers)) {

View File

@ -173,21 +173,18 @@ func TestEnnoblementSync(t *testing.T) {
require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit))
for {
servers, err := serverRepo.List(ctx, listParams)
res, err := serverRepo.List(ctx, listParams)
require.NoError(collect, err)
if len(servers) == 0 {
break
}
for _, s := range servers {
for _, s := range res.Servers() {
assert.WithinDuration(collect, time.Now(), s.EnnoblementDataSyncedAt(), time.Minute)
}
require.NoError(collect, listParams.SetKeyGT(domain.NullString{
Value: servers[len(servers)-1].Key(),
Valid: true,
}))
if res.Next().IsZero() {
break
}
require.NoError(collect, listParams.SetCursor(res.Next()))
}
}, 30*time.Second, time.Second, "servers")

View File

@ -157,22 +157,19 @@ func TestSnapshotCreation(t *testing.T) {
require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit))
for {
servers, err := serverRepo.List(ctx, listParams)
res, err := serverRepo.List(ctx, listParams)
require.NoError(collect, err)
if len(servers) == 0 {
return
}
for _, s := range servers {
for _, s := range res.Servers() {
assert.WithinDuration(collect, time.Now(), s.PlayerSnapshotsCreatedAt(), time.Minute, s.Key())
assert.WithinDuration(collect, time.Now(), s.TribeSnapshotsCreatedAt(), time.Minute, s.Key())
}
require.NoError(collect, listParams.SetKeyGT(domain.NullString{
Value: servers[len(servers)-1].Key(),
Valid: true,
}))
if res.Next().IsZero() {
return
}
require.NoError(collect, listParams.SetCursor(res.Next()))
}
}, 30*time.Second, 500*time.Millisecond, "servers")