diff --git a/internal/adapter/repository_bun_village.go b/internal/adapter/repository_bun_village.go index 83e692f..2921745 100644 --- a/internal/adapter/repository_bun_village.go +++ b/internal/adapter/repository_bun_village.go @@ -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) } diff --git a/internal/adapter/repository_player_test.go b/internal/adapter/repository_player_test.go index a5c9490..cb7218e 100644 --- a/internal/adapter/repository_player_test.go +++ b/internal/adapter/repository_player_test.go @@ -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() diff --git a/internal/adapter/repository_server_test.go b/internal/adapter/repository_server_test.go index 00d29fc..d2878df 100644 --- a/internal/adapter/repository_server_test.go +++ b/internal/adapter/repository_server_test.go @@ -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) { diff --git a/internal/adapter/repository_test.go b/internal/adapter/repository_test.go index 337c160..86ba068 100644 --- a/internal/adapter/repository_test.go +++ b/internal/adapter/repository_test.go @@ -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 } diff --git a/internal/adapter/repository_tribe_test.go b/internal/adapter/repository_tribe_test.go index 2e10f52..871589a 100644 --- a/internal/adapter/repository_tribe_test.go +++ b/internal/adapter/repository_tribe_test.go @@ -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) { diff --git a/internal/adapter/repository_version_test.go b/internal/adapter/repository_version_test.go index 2f2b745..6b89736 100644 --- a/internal/adapter/repository_version_test.go +++ b/internal/adapter/repository_version_test.go @@ -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) { diff --git a/internal/adapter/repository_village_test.go b/internal/adapter/repository_village_test.go index 7e7c3ea..b7fac47 100644 --- a/internal/adapter/repository_village_test.go +++ b/internal/adapter/repository_village_test.go @@ -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()) { diff --git a/internal/app/service_village.go b/internal/app/service_village.go index 05128a0..08dd278 100644 --- a/internal/app/service_village.go +++ b/internal/app/service_village.go @@ -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) +} diff --git a/internal/domain/domaintest/village.go b/internal/domain/domaintest/village.go index 862fec3..1f99e5f 100644 --- a/internal/domain/domaintest/village.go +++ b/internal/domain/domaintest/village.go @@ -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 diff --git a/internal/domain/player.go b/internal/domain/player.go index 1a1ea95..5727cd9 100644 --- a/internal/domain/player.go +++ b/internal/domain/player.go @@ -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 diff --git a/internal/domain/player_test.go b/internal/domain/player_test.go index 5a7b914..8e4115d 100644 --- a/internal/domain/player_test.go +++ b/internal/domain/player_test.go @@ -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()) }) } } diff --git a/internal/domain/tribe.go b/internal/domain/tribe.go index d08e67d..b41d95a 100644 --- a/internal/domain/tribe.go +++ b/internal/domain/tribe.go @@ -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 diff --git a/internal/domain/utils.go b/internal/domain/utils.go index b2142f2..090e1c0 100644 --- a/internal/domain/utils.go +++ b/internal/domain/utils.go @@ -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 } diff --git a/internal/domain/village.go b/internal/domain/village.go index 893ecb5..05fa168 100644 --- a/internal/domain/village.go +++ b/internal/domain/village.go @@ -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 } diff --git a/internal/domain/village_test.go b/internal/domain/village_test.go index d797722..0502a2e 100644 --- a/internal/domain/village_test.go +++ b/internal/domain/village_test.go @@ -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()) + }) } diff --git a/internal/port/consumer_data_sync_test.go b/internal/port/consumer_data_sync_test.go index 0b7641c..4b54d29 100644 --- a/internal/port/consumer_data_sync_test.go +++ b/internal/port/consumer_data_sync_test.go @@ -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)) {