refactor: tribe - cursor pagination (#60)

Reviewed-on: twhelp/corev3#60
This commit is contained in:
Dawid Wysokiński 2024-02-12 07:07:15 +00:00
parent 6a4e896cd4
commit 25efaacc01
14 changed files with 651 additions and 251 deletions

View File

@ -111,17 +111,25 @@ func (repo *TribeBunRepository) UpdateDominance(ctx context.Context, serverKey s
return nil
}
func (repo *TribeBunRepository) List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error) {
func (repo *TribeBunRepository) List(
ctx context.Context,
params domain.ListTribesParams,
) (domain.ListTribesResult, error) {
var tribes bunmodel.Tribes
if err := repo.db.NewSelect().
Model(&tribes).
Apply(listTribesParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select tribes from the db: %w", err)
return domain.ListTribesResult{}, fmt.Errorf("couldn't select tribes from the db: %w", err)
}
return tribes.ToDomain()
converted, err := tribes.ToDomain()
if err != nil {
return domain.ListTribesResult{}, err
}
return domain.NewListTribesResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
@ -153,10 +161,6 @@ func (a listTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
q = q.Where("tribe.id IN (?)", bun.In(ids))
}
if idGT := a.params.IDGT(); idGT.Valid {
q = q.Where("tribe.id > ?", idGT.Value)
}
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("tribe.server_key IN (?)", bun.In(serverKeys))
}
@ -184,5 +188,93 @@ func (a listTribesParamsApplier) 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)
}
//nolint:gocyclo
func (a listTribesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery {
if a.params.Cursor().IsZero() {
return q
}
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
cursorID := a.params.Cursor().ID()
cursorServerKey := a.params.Cursor().ServerKey()
sort := a.params.Sort()
sortLen := len(sort)
// based on https://github.com/prisma/prisma/issues/19159#issuecomment-1713389245
switch {
case sortLen == 1:
switch sort[0] {
case domain.TribeSortIDASC:
q = q.Where("tribe.id >= ?", cursorID)
case domain.TribeSortIDDESC:
q = q.Where("tribe.id <= ?", cursorID)
case domain.TribeSortServerKeyASC, domain.TribeSortServerKeyDESC:
return q.Err(errSortNoUniqueField)
default:
return q.Err(errUnsupportedSortValue)
}
case sortLen > 1:
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
switch sort[0] {
case domain.TribeSortIDASC:
q = q.Where("tribe.id > ?", cursorID)
case domain.TribeSortIDDESC:
q = q.Where("tribe.id < ?", cursorID)
case domain.TribeSortServerKeyASC:
q = q.Where("tribe.server_key > ?", cursorServerKey)
case domain.TribeSortServerKeyDESC:
q = q.Where("tribe.server_key < ?", cursorServerKey)
default:
return q.Err(errUnsupportedSortValue)
}
for i := 1; i < sortLen; i++ {
q.WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
current := sort[i]
for j := 0; j < i; j++ {
s := sort[j]
switch s {
case domain.TribeSortIDASC,
domain.TribeSortIDDESC:
q = q.Where("tribe.id = ?", cursorID)
case domain.TribeSortServerKeyASC,
domain.TribeSortServerKeyDESC:
q = q.Where("tribe.server_key = ?", cursorServerKey)
default:
return q.Err(errUnsupportedSortValue)
}
}
switch current {
case domain.TribeSortIDASC:
q = q.Where("tribe.id >= ?", cursorID)
case domain.TribeSortIDDESC:
q = q.Where("tribe.id <= ?", cursorID)
case domain.TribeSortServerKeyASC:
q = q.Where("tribe.server_key >= ?", cursorServerKey)
case domain.TribeSortServerKeyDESC:
q = q.Where("tribe.server_key <= ?", cursorServerKey)
default:
return q.Err(errUnsupportedSortValue)
}
return q
})
}
return q
})
}
return q
})
return q
}

View File

@ -129,6 +129,8 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assert.True(t, slices.IsSortedFunc(servers, func(a, b domain.Server) int {
return cmp.Compare(a.Key(), b.Key())
}))
assert.False(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()
@ -486,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.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()

View File

@ -24,7 +24,7 @@ type serverRepository interface {
type tribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error
List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error)
List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error)
Delete(ctx context.Context, serverKey string, ids ...int) error
}

View File

@ -91,8 +91,9 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
Valid: true,
}))
tribes, err := repos.tribe.List(ctx, listTribesParams)
res, err := repos.tribe.List(ctx, listTribesParams)
require.NoError(t, err)
tribes := res.Tribes()
require.NotEmpty(t, tribes)
date := time.Now()

View File

@ -3,7 +3,6 @@ package adapter_test
import (
"cmp"
"context"
"fmt"
"math"
"slices"
"testing"
@ -39,8 +38,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
require.NoError(t, listParams.SetIDs(ids))
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
tribes, err := repos.tribe.List(ctx, listParams)
res, err := repos.tribe.List(ctx, listParams)
require.NoError(t, err)
tribes := res.Tribes()
assert.Len(t, tribes, len(params))
for i, p := range params {
idx := slices.IndexFunc(tribes, func(tribe domain.Tribe) bool {
@ -150,10 +150,10 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}))
require.NoError(t, listTribesParams.SetServerKeys([]string{tt.serverKey}))
tribes, err := repos.tribe.List(ctx, listTribesParams)
res, err := repos.tribe.List(ctx, listTribesParams)
require.NoError(t, err)
assert.NotEmpty(t, tribes)
for _, tr := range tribes {
assert.NotEmpty(t, res)
for _, tr := range res.Tribes() {
if tt.numPlayerVillages == 0 {
assert.InDelta(t, 0.0, tr.Dominance(), 0.001)
continue
@ -170,17 +170,11 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
repos := newRepos(t)
tribes, listTribesErr := repos.tribe.List(ctx, domain.NewListTribesParams())
require.NoError(t, listTribesErr)
require.NotEmpty(t, tribes)
randTribe := tribes[0]
tests := []struct {
name string
params func(t *testing.T) domain.ListTribesParams
assertTribes func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes)
assertResult func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListTribesParams, total int)
}{
{
name: "OK: default params",
@ -188,8 +182,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Helper()
return domain.NewListTribesParams()
},
assertTribes: func(t *testing.T, _ domain.ListTribesParams, tribes domain.Tribes) {
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
@ -197,15 +192,13 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
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.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[serverKey DESC, id DESC]",
@ -215,8 +208,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
require.NoError(t, params.SetSort([]domain.TribeSort{domain.TribeSortServerKeyDESC, domain.TribeSortIDDESC}))
return params
},
assertTribes: func(t *testing.T, _ domain.ListTribesParams, tribes domain.Tribes) {
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
@ -229,26 +223,32 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: ids=[%d] serverKeys=[%s]", randTribe.ID(), randTribe.ServerKey()),
name: "OK: ids serverKeys",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, len(res.Tribes()))
randTribe := res.Tribes()[0]
require.NoError(t, params.SetIDs([]int{randTribe.ID()}))
require.NoError(t, params.SetServerKeys([]string{randTribe.ServerKey()}))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
ids := params.IDs()
serverKeys := params.ServerKeys()
tribes := res.Tribes()
assert.NotEmpty(t, tribes)
for _, tr := range tribes {
assert.True(t, slices.Contains(ids, tr.ID()))
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))
@ -258,37 +258,6 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: idGT=%d", randTribe.ID()),
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetIDGT(domain.NullInt{
Value: randTribe.ID(),
Valid: true,
}))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
t.Helper()
assert.NotEmpty(t, tribes)
for _, tr := range tribes {
assert.Greater(t, tr.ID(), params.IDGT().Value, tr.ID())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: deleted=true",
@ -301,8 +270,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}))
return params
},
assertTribes: func(t *testing.T, _ domain.ListTribesParams, tribes domain.Tribes) {
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, tribes)
for _, s := range tribes {
assert.True(t, s.IsDeleted())
@ -312,10 +282,6 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: deleted=false",
@ -328,8 +294,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
}))
return params
},
assertTribes: func(t *testing.T, _ domain.ListTribesParams, tribes domain.Tribes) {
assertResult: func(t *testing.T, _ domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, tribes)
for _, s := range tribes {
assert.False(t, s.IsDeleted())
@ -339,31 +306,146 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: offset=1 limit=2",
name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Tribes()), 2)
require.NoError(t, params.SetSort([]domain.TribeSort{domain.TribeSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.Tribes()[1].ServerKey()}))
require.NoError(t, params.SetCursor(domaintest.NewTribeCursor(t, func(cfg *domaintest.TribeCursorConfig) {
cfg.ID = res.Tribes()[1].ID()
})))
return params
},
assertTribes: func(t *testing.T, params domain.ListTribesParams, tribes domain.Tribes) {
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
assert.Len(t, tribes, params.Limit())
serverKeys := params.ServerKeys()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
for _, tr := range res.Tribes() {
assert.GreaterOrEqual(t, tr.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, tr.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribesParams, total int) {
},
{
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortServerKeyASC,
domain.TribeSortIDASC,
}))
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Tribes()), 2)
require.NoError(t, params.SetCursor(domaintest.NewTribeCursor(t, func(cfg *domaintest.TribeCursorConfig) {
cfg.ID = res.Tribes()[1].ID()
cfg.ServerKey = res.Tribes()[1].ServerKey()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, tribes[0].ID(), params.Cursor().ID())
for _, tr := range res.Tribes() {
assert.GreaterOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetSort([]domain.TribeSort{
domain.TribeSortServerKeyDESC,
domain.TribeSortIDDESC,
}))
res, err := repos.tribe.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.Tribes()), 2)
require.NoError(t, params.SetCursor(domaintest.NewTribeCursor(t, func(cfg *domaintest.TribeCursorConfig) {
cfg.ID = res.Tribes()[1].ID()
cfg.ServerKey = res.Tribes()[1].ServerKey()
})))
return params
},
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
tribes := res.Tribes()
assert.NotEmpty(t, len(tribes))
assert.True(t, slices.IsSortedFunc(tribes, func(a, b domain.Tribe) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, tribes[0].ID(), params.Cursor().ID())
for _, tr := range res.Tribes() {
assert.LessOrEqual(t, tr.ServerKey(), params.Cursor().ServerKey())
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListTribesParams {
t.Helper()
params := domain.NewListTribesParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(t *testing.T, params domain.ListTribesParams, res domain.ListTribesResult) {
t.Helper()
assert.Len(t, res.Tribes(), params.Limit())
assert.False(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
},
}
@ -378,7 +460,7 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
res, err := repos.tribe.List(ctx, params)
tt.assertError(t, err)
tt.assertTribes(t, params, res)
tt.assertResult(t, params, res)
})
}
})
@ -406,8 +488,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
require.NoError(t, listTribesParams.SetDeleted(domain.NullBool{Value: false, Valid: true}))
require.NoError(t, listTribesParams.SetServerKeys(serverKeys))
tribes, err := repos.tribe.List(ctx, listTribesParams)
res, err := repos.tribe.List(ctx, listTribesParams)
require.NoError(t, err)
tribes := res.Tribes()
var serverKey string
var ids []int
@ -430,9 +513,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
require.NoError(t, listTribesParams.SetDeleted(domain.NullBool{Valid: false}))
require.NoError(t, listTribesParams.SetServerKeys(serverKeys))
tribes, err = repos.tribe.List(ctx, listTribesParams)
res, err = repos.tribe.List(ctx, listTribesParams)
require.NoError(t, err)
for _, tr := range tribes {
for _, tr := range res.Tribes() {
if tr.ServerKey() == serverKey && slices.Contains(ids, tr.ID()) {
if slices.Contains(idsToDelete, tr.ID()) {
assert.WithinDuration(t, time.Now(), tr.DeletedAt(), time.Minute)

View File

@ -40,6 +40,7 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
assert.True(t, slices.IsSortedFunc(res.Versions(), func(a, b domain.Version) int {
return cmp.Compare(a.Code(), b.Code())
}))
assert.False(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
},
assertError: func(t *testing.T, err error) {

View File

@ -10,7 +10,7 @@ import (
type TribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error
List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error)
List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error)
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
//
// https://en.wiktionary.org/wiki/soft_deletion
@ -98,12 +98,12 @@ func (svc *TribeService) createOrUpdateChunk(ctx context.Context, serverKey stri
return err
}
storedTribes, err := svc.repo.List(ctx, listParams)
res, err := svc.repo.List(ctx, listParams)
if err != nil {
return err
}
createParams, err := domain.NewCreateTribeParams(serverKey, tribes, storedTribes)
createParams, err := domain.NewCreateTribeParams(serverKey, tribes, res.Tribes())
if err != nil {
return err
}
@ -132,21 +132,17 @@ func (svc *TribeService) delete(ctx context.Context, serverKey string, tribes do
var toDelete []int
for {
storedTribes, err := svc.repo.List(ctx, listParams)
res, err := svc.repo.List(ctx, listParams)
if err != nil {
return err
}
toDelete = append(toDelete, res.Tribes().Delete(serverKey, tribes)...)
if len(storedTribes) == 0 {
if res.Next().IsZero() {
break
}
toDelete = append(toDelete, storedTribes.Delete(serverKey, tribes)...)
if err = listParams.SetIDGT(domain.NullInt{
Value: storedTribes[len(storedTribes)-1].ID(),
Valid: true,
}); err != nil {
if err = listParams.SetCursor(res.Next()); err != nil {
return err
}
}
@ -158,6 +154,6 @@ func (svc *TribeService) UpdateDominance(ctx context.Context, payload domain.Vil
return svc.repo.UpdateDominance(ctx, payload.ServerKey(), payload.NumPlayerVillages())
}
func (svc *TribeService) List(ctx context.Context, params domain.ListTribesParams) (domain.Tribes, error) {
func (svc *TribeService) List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error) {
return svc.repo.List(ctx, params)
}

View File

@ -53,10 +53,11 @@ func (svc *TribeSnapshotService) Create(
}
for {
tribes, err := svc.tribeSvc.List(ctx, listTribesParams)
res, err := svc.tribeSvc.List(ctx, listTribesParams)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
tribes := res.Tribes()
if len(tribes) == 0 {
break
@ -71,10 +72,11 @@ func (svc *TribeSnapshotService) Create(
return fmt.Errorf("%s: %w", serverKey, err)
}
if err = listTribesParams.SetIDGT(domain.NullInt{
Value: tribes[len(tribes)-1].ID(),
Valid: true,
}); err != nil {
if res.Next().IsZero() {
break
}
if err = listTribesParams.SetCursor(res.Next()); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
}

View File

@ -30,10 +30,10 @@ func NewServerCursor(tb TestingTB, opts ...func(cfg *ServerCursorConfig)) domain
opt(cfg)
}
vc, err := domain.NewServerCursor(cfg.Key, cfg.Open)
sc, err := domain.NewServerCursor(cfg.Key, cfg.Open)
require.NoError(tb, err)
return vc
return sc
}
type ServerConfig struct {

View File

@ -12,6 +12,29 @@ func RandTribeTag() string {
return gofakeit.LetterN(5)
}
type TribeCursorConfig struct {
ID int
ServerKey string
}
func NewTribeCursor(tb TestingTB, opts ...func(cfg *TribeCursorConfig)) domain.TribeCursor {
tb.Helper()
cfg := &TribeCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
}
for _, opt := range opts {
opt(cfg)
}
tc, err := domain.NewTribeCursor(cfg.ID, cfg.ServerKey)
require.NoError(tb, err)
return tc
}
type TribeConfig struct {
ID int
ServerKey string

View File

@ -6,6 +6,7 @@ import (
"math"
"net/url"
"slices"
"strconv"
"time"
)
@ -368,14 +369,85 @@ const (
TribeSortServerKeyDESC
)
type TribeCursor struct {
id int
serverKey string
}
const tribeCursorModelName = "TribeCursor"
func NewTribeCursor(id int, serverKey string) (TribeCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return TribeCursor{}, ValidationError{
Model: tribeCursorModelName,
Field: "serverKey",
Err: err,
}
}
return TribeCursor{
id: id,
serverKey: serverKey,
}, nil
}
func decodeTribeCursor(encoded string) (TribeCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return TribeCursor{}, err
}
id, err := strconv.Atoi(m["id"])
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
tc, err := NewTribeCursor(id, m["serverKey"])
if err != nil {
return TribeCursor{}, ErrInvalidCursor
}
return tc, nil
}
func (tc TribeCursor) ID() int {
return tc.id
}
func (tc TribeCursor) ServerKey() string {
return tc.serverKey
}
func (tc TribeCursor) IsZero() bool {
return tc == TribeCursor{}
}
func (tc TribeCursor) Encode() string {
if tc.IsZero() {
return ""
}
return encodeCursor([][2]string{
{"id", strconv.Itoa(tc.id)},
{"serverKey", tc.serverKey},
})
}
type ListTribesParams struct {
ids []int
idGT NullInt
serverKeys []string
deleted NullBool
sort []TribeSort
cursor TribeCursor
limit int
offset int
}
const (
@ -414,26 +486,6 @@ func (params *ListTribesParams) SetIDs(ids []int) error {
return nil
}
func (params *ListTribesParams) IDGT() NullInt {
return params.idGT
}
func (params *ListTribesParams) SetIDGT(idGT NullInt) error {
if idGT.Valid {
if err := validateIntInRange(idGT.Value, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "idGT",
Err: err,
}
}
}
params.idGT = idGT
return nil
}
func (params *ListTribesParams) ServerKeys() []string {
return params.serverKeys
}
@ -475,6 +527,30 @@ func (params *ListTribesParams) SetSort(sort []TribeSort) error {
return nil
}
func (params *ListTribesParams) Cursor() TribeCursor {
return params.cursor
}
func (params *ListTribesParams) SetCursor(cursor TribeCursor) error {
params.cursor = cursor
return nil
}
func (params *ListTribesParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeTribeCursor(encoded)
if err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListTribesParams) Limit() int {
return params.limit
}
@ -493,20 +569,53 @@ func (params *ListTribesParams) SetLimit(limit int) error {
return nil
}
func (params *ListTribesParams) Offset() int {
return params.offset
type ListTribesResult struct {
tribes Tribes
self TribeCursor
next TribeCursor
}
func (params *ListTribesParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listTribesParamsModelName,
Field: "offset",
Err: err,
const listTribesResultModelName = "ListTribesResult"
func NewListTribesResult(tribes Tribes, next Tribe) (ListTribesResult, error) {
var err error
res := ListTribesResult{
tribes: tribes,
}
if len(tribes) > 0 {
res.self, err = NewTribeCursor(tribes[0].ID(), tribes[0].ServerKey())
if err != nil {
return ListTribesResult{}, ValidationError{
Model: listTribesResultModelName,
Field: "self",
Err: err,
}
}
}
params.offset = offset
if !next.IsZero() {
res.next, err = NewTribeCursor(next.ID(), next.ServerKey())
if err != nil {
return ListTribesResult{}, ValidationError{
Model: listTribesResultModelName,
Field: "next",
Err: err,
}
}
}
return nil
return res, nil
}
func (res ListTribesResult) Tribes() Tribes {
return res.tribes
}
func (res ListTribesResult) Self() TribeCursor {
return res.self
}
func (res ListTribesResult) Next() TribeCursor {
return res.next
}

View File

@ -9,6 +9,7 @@ import (
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -179,6 +180,81 @@ func TestNewCreateTribeParams(t *testing.T) {
}
}
func TestNewTribeCursor(t *testing.T) {
t.Parallel()
validTribeCursor := domaintest.NewTribeCursor(t)
type args struct {
id int
serverKey string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validTribeCursor.ID(),
serverKey: validTribeCursor.ServerKey(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validTribeCursor.ServerKey(),
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validTribeCursor.ID(),
serverKey: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "TribeCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tc, err := domain.NewTribeCursor(tt.args.id, tt.args.serverKey)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, tc.ID())
assert.Equal(t, tt.args.serverKey, tc.ServerKey())
assert.NotEmpty(t, tc.Encode())
})
}
}
func TestListTribesParams_SetIDs(t *testing.T) {
t.Parallel()
@ -241,63 +317,6 @@ func TestListTribesParams_SetIDs(t *testing.T) {
}
}
func TestListTribesParams_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{
Value: domaintest.RandID(),
Valid: true,
},
},
},
{
name: "ERR: value < 0",
args: args{
idGT: domain.NullInt{
Value: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "idGT",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetIDGT(tt.args.idGT), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.idGT, params.IDGT())
})
}
}
func TestListTribesParams_SetSort(t *testing.T) {
t.Parallel()
@ -372,6 +391,90 @@ func TestListTribesParams_SetSort(t *testing.T) {
}
}
func TestListTribesParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewTribeCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.TribeCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
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: "ListTribesParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListTribesParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedCursor.ID(), params.Cursor().ID())
assert.Equal(t, tt.expectedCursor.ServerKey(), params.Cursor().ServerKey())
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListTribesParams_SetLimit(t *testing.T) {
t.Parallel()
@ -437,53 +540,46 @@ func TestListTribesParams_SetLimit(t *testing.T) {
}
}
func TestListTribesParams_SetOffset(t *testing.T) {
func TestNewListTribesResult(t *testing.T) {
t.Parallel()
type args struct {
offset int
servers := domain.Tribes{
domaintest.NewTribe(t),
domaintest.NewTribe(t),
domaintest.NewTribe(t),
}
next := domaintest.NewTribe(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: "ListTribesParams",
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.NewListTribesResult(servers, next)
require.NoError(t, err)
assert.Equal(t, servers, res.Tribes())
assert.Equal(t, servers[0].ID(), res.Self().ID())
assert.Equal(t, servers[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
})
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
params := domain.NewListTribesParams()
res, err := domain.NewListTribesResult(servers, domain.Tribe{})
require.NoError(t, err)
assert.Equal(t, servers, res.Tribes())
assert.Equal(t, servers[0].ID(), res.Self().ID())
assert.Equal(t, servers[0].ServerKey(), res.Self().ServerKey())
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.NewListTribesResult(nil, domain.Tribe{})
require.NoError(t, err)
assert.Zero(t, res.Tribes())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}

View File

@ -290,19 +290,16 @@ func TestDataSync(t *testing.T) {
allTribes := make(domain.Tribes, 0, len(expectedTribes))
for {
tribes, err := tribeRepo.List(ctx, listParams)
res, err := tribeRepo.List(ctx, listParams)
require.NoError(collect, err)
if len(tribes) == 0 {
allTribes = append(allTribes, res.Tribes()...)
if res.Next().IsZero() {
break
}
allTribes = append(allTribes, tribes...)
require.NoError(collect, listParams.SetIDGT(domain.NullInt{
Value: tribes[len(tribes)-1].ID(),
Valid: true,
}))
require.NoError(collect, listParams.SetCursor(res.Next()))
}
if !assert.Len(collect, allTribes, len(expectedTribes)) {

View File

@ -190,19 +190,16 @@ func TestSnapshotCreation(t *testing.T) {
var allTribes domain.Tribes
for {
tribes, err := tribeRepo.List(ctx, listTribesParams)
res, err := tribeRepo.List(ctx, listTribesParams)
require.NoError(collect, err)
if len(tribes) == 0 {
allTribes = append(allTribes, res.Tribes()...)
if res.Next().IsZero() {
break
}
allTribes = append(allTribes, tribes...)
require.NoError(collect, listTribesParams.SetIDGT(domain.NullInt{
Value: tribes[len(tribes)-1].ID(),
Valid: true,
}))
require.NoError(collect, listTribesParams.SetCursor(res.Next()))
}
listSnapshotsParams := domain.NewListTribeSnapshotsParams()