refactor: tribe change - cursor pagination (#25)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#25
This commit is contained in:
Dawid Wysokiński 2024-03-12 06:19:16 +00:00
parent 6901c551e8
commit 53596dcdad
13 changed files with 683 additions and 190 deletions

View File

@ -52,17 +52,22 @@ func (repo *TribeChangeBunRepository) Create(ctx context.Context, params ...doma
func (repo *TribeChangeBunRepository) List(
ctx context.Context,
params domain.ListTribeChangesParams,
) (domain.TribeChanges, error) {
) (domain.ListTribeChangesResult, error) {
var tribeChanges bunmodel.TribeChanges
if err := repo.db.NewSelect().
Model(&tribeChanges).
Apply(listTribeChangesParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select tribe changes from the db: %w", err)
return domain.ListTribeChangesResult{}, fmt.Errorf("couldn't select tribe changes from the db: %w", err)
}
return tribeChanges.ToDomain()
converted, err := tribeChanges.ToDomain()
if err != nil {
return domain.ListTribeChangesResult{}, err
}
return domain.NewListTribeChangesResult(separateListResultAndNext(converted, params.Limit()))
}
type listTribeChangesParamsApplier struct {
@ -94,5 +99,57 @@ func (a listTribeChangesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuer
}
}
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
}
func (a listTribeChangesParamsApplier) 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.TribeChangeSortIDASC:
el.value = cursor.ID()
el.unique = true
el.column = "tc.id"
el.direction = sortDirectionASC
case domain.TribeChangeSortIDDESC:
el.value = cursor.ID()
el.unique = true
el.column = "tc.id"
el.direction = sortDirectionDESC
case domain.TribeChangeSortServerKeyASC:
el.value = cursor.ServerKey()
el.column = "tc.server_key"
el.direction = sortDirectionASC
case domain.TribeChangeSortServerKeyDESC:
el.value = cursor.ServerKey()
el.column = "tc.server_key"
el.direction = sortDirectionDESC
case domain.TribeChangeSortCreatedAtASC:
el.value = cursor.CreatedAt()
el.column = "tc.created_at"
el.direction = sortDirectionASC
case domain.TribeChangeSortCreatedAtDESC:
el.value = cursor.CreatedAt()
el.column = "tc.created_at"
el.direction = sortDirectionDESC
default:
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)
}
return q.Apply(cursorApplier.apply)
}

View File

@ -431,12 +431,9 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
require.NoError(t, params.SetSort([]domain.EnnoblementSort{domain.EnnoblementSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.Ennoblements()[1].ServerKey()}))
require.NoError(
t,
params.SetCursor(domaintest.NewEnnoblementCursor(t, func(cfg *domaintest.EnnoblementCursorConfig) {
cfg.ID = res.Ennoblements()[1].ID()
})),
)
cursor, err := res.Ennoblements()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -475,13 +472,9 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
require.NoError(t, err)
require.Greater(t, len(res.Ennoblements()), 2)
require.NoError(
t,
params.SetCursor(domaintest.NewEnnoblementCursor(t, func(cfg *domaintest.EnnoblementCursorConfig) {
cfg.ID = res.Ennoblements()[1].ID()
cfg.ServerKey = res.Ennoblements()[1].ServerKey()
})),
)
cursor, err := res.Ennoblements()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -520,13 +513,9 @@ func testEnnoblementRepository(t *testing.T, newRepos func(t *testing.T) reposit
require.NoError(t, err)
require.Greater(t, len(res.Ennoblements()), 2)
require.NoError(
t,
params.SetCursor(domaintest.NewEnnoblementCursor(t, func(cfg *domaintest.EnnoblementCursorConfig) {
cfg.ID = res.Ennoblements()[1].ID()
cfg.ServerKey = res.Ennoblements()[1].ServerKey()
})),
)
cursor, err := res.Ennoblements()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},

View File

@ -531,9 +531,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, params.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.Players()[1].ServerKey()}))
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
cfg.ID = res.Players()[1].ID()
})))
cursor, err := res.Players()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -572,10 +572,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, err)
require.Greater(t, len(res.Players()), 2)
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
cfg.ID = res.Players()[1].ID()
cfg.ServerKey = res.Players()[1].ServerKey()
})))
cursor, err := res.Players()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -614,10 +613,9 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
require.NoError(t, err)
require.Greater(t, len(res.Players()), 2)
require.NoError(t, params.SetCursor(domaintest.NewPlayerCursor(t, func(cfg *domaintest.PlayerCursorConfig) {
cfg.ID = res.Players()[1].ID()
cfg.ServerKey = res.Players()[1].ServerKey()
})))
cursor, err := res.Players()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},

View File

@ -369,9 +369,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
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()
})))
cursor, err := res.Servers()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -403,10 +403,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
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()
})))
cursor, err := res.Servers()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -446,10 +445,9 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
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()
})))
cursor, err := res.Servers()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},

View File

@ -56,7 +56,7 @@ type ennoblementRepository interface {
type tribeChangeRepository interface {
Create(ctx context.Context, params ...domain.CreateTribeChangeParams) error
List(ctx context.Context, params domain.ListTribeChangesParams) (domain.TribeChanges, error)
List(ctx context.Context, params domain.ListTribeChangesParams) (domain.ListTribeChangesResult, error)
}
type tribeSnapshotRepository interface {

View File

@ -32,8 +32,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
listParams := domain.NewListTribeChangesParams()
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
tribeChanges, err := repos.tribeChange.List(ctx, listParams)
res, err := repos.tribeChange.List(ctx, listParams)
require.NoError(t, err)
tribeChanges := res.TribeChanges()
for i, p := range params {
idx := slices.IndexFunc(tribeChanges, func(tc domain.TribeChange) bool {
return tc.ServerKey() == p.ServerKey() &&
@ -60,8 +61,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
listParams := domain.NewListTribeChangesParams()
require.NoError(t, listParams.SetServerKeys([]string{params[0].ServerKey()}))
tribeChanges, err := repos.tribeChange.List(ctx, listParams)
res, err := repos.tribeChange.List(ctx, listParams)
require.NoError(t, err)
tribeChanges := res.TribeChanges()
m := make(map[string][]int)
@ -139,17 +141,11 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
repos := newRepos(t)
tribeChanges, listTribeChangesErr := repos.tribeChange.List(ctx, domain.NewListTribeChangesParams())
require.NoError(t, listTribeChangesErr)
require.NotEmpty(t, tribeChanges)
randTribeChange := tribeChanges[0]
tests := []struct {
name string
params func(t *testing.T) domain.ListTribeChangesParams
assertTribeChanges func(t *testing.T, params domain.ListTribeChangesParams, tribeChanges domain.TribeChanges)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListTribeChangesParams, total int)
name string
params func(t *testing.T) domain.ListTribeChangesParams
assertResult func(t *testing.T, params domain.ListTribeChangesParams, res domain.ListTribeChangesResult)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
@ -157,8 +153,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
return domain.NewListTribeChangesParams()
},
assertTribeChanges: func(t *testing.T, _ domain.ListTribeChangesParams, tribeChanges domain.TribeChanges) {
assertResult: func(t *testing.T, _ domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
tribeChanges := res.TribeChanges()
assert.NotEmpty(t, len(tribeChanges))
assert.True(t, slices.IsSortedFunc(tribeChanges, func(a, b domain.TribeChange) int {
return cmp.Or(
@ -167,15 +164,13 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
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.ListTribeChangesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[serverKey DESC, createdAt DESC]",
@ -188,8 +183,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
}))
return params
},
assertTribeChanges: func(t *testing.T, _ domain.ListTribeChangesParams, tribeChanges domain.TribeChanges) {
assertResult: func(t *testing.T, _ domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
tribeChanges := res.TribeChanges()
assert.NotEmpty(t, len(tribeChanges))
assert.True(t, slices.IsSortedFunc(tribeChanges, func(a, b domain.TribeChange) int {
return cmp.Or(
@ -202,10 +198,6 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribeChangesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[id ASC]",
@ -217,8 +209,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
}))
return params
},
assertTribeChanges: func(t *testing.T, _ domain.ListTribeChangesParams, tribeChanges domain.TribeChanges) {
assertResult: func(t *testing.T, _ domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
tribeChanges := res.TribeChanges()
assert.NotEmpty(t, len(tribeChanges))
assert.True(t, slices.IsSortedFunc(tribeChanges, func(a, b domain.TribeChange) int {
return cmp.Compare(a.ID(), b.ID())
@ -228,10 +221,6 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribeChangesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[id DESC]",
@ -243,8 +232,9 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
}))
return params
},
assertTribeChanges: func(t *testing.T, _ domain.ListTribeChangesParams, tribeChanges domain.TribeChanges) {
assertResult: func(t *testing.T, _ domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
tribeChanges := res.TribeChanges()
assert.NotEmpty(t, len(tribeChanges))
assert.True(t, slices.IsSortedFunc(tribeChanges, func(a, b domain.TribeChange) int {
return cmp.Compare(a.ID(), b.ID()) * -1
@ -254,25 +244,31 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribeChangesParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: serverKeys=[%s]", randTribeChange.ServerKey()),
name: "OK: serverKeys",
params: func(t *testing.T) domain.ListTribeChangesParams {
t.Helper()
params := domain.NewListTribeChangesParams()
require.NoError(t, params.SetServerKeys([]string{randTribeChange.ServerKey()}))
res, err := repos.tribeChange.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.TribeChanges())
randTC := res.TribeChanges()[0]
require.NoError(t, params.SetServerKeys([]string{randTC.ServerKey()}))
return params
},
assertTribeChanges: func(t *testing.T, params domain.ListTribeChangesParams, tribeChanges domain.TribeChanges) {
assertResult: func(t *testing.T, params domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
serverKeys := params.ServerKeys()
for _, tc := range tribeChanges {
tcs := res.TribeChanges()
assert.NotZero(t, tcs)
for _, tc := range tcs {
assert.True(t, slices.Contains(serverKeys, tc.ServerKey()))
}
},
@ -280,31 +276,145 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, _ domain.ListTribeChangesParams, 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.ListTribeChangesParams {
t.Helper()
params := domain.NewListTribeChangesParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
res, err := repos.tribeChange.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.TribeChanges()), 2)
require.NoError(t, params.SetSort([]domain.TribeChangeSort{domain.TribeChangeSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.TribeChanges()[1].ServerKey()}))
cursor, err := res.TribeChanges()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertTribeChanges: func(t *testing.T, params domain.ListTribeChangesParams, tribeChanges domain.TribeChanges) {
assertResult: func(t *testing.T, params domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
assert.Len(t, tribeChanges, params.Limit())
serverKeys := params.ServerKeys()
tcs := res.TribeChanges()
assert.NotEmpty(t, len(tcs))
for _, tc := range tcs {
assert.GreaterOrEqual(t, tc.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, tc.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(tcs, func(a, b domain.TribeChange) 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.ListTribeChangesParams, total int) {
},
{
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListTribeChangesParams {
t.Helper()
assert.NotEmpty(t, total)
params := domain.NewListTribeChangesParams()
require.NoError(t, params.SetSort([]domain.TribeChangeSort{
domain.TribeChangeSortServerKeyASC,
domain.TribeChangeSortIDASC,
}))
res, err := repos.tribeChange.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.TribeChanges()), 2)
cursor, err := res.TribeChanges()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
tcs := res.TribeChanges()
assert.NotEmpty(t, len(tcs))
assert.True(t, slices.IsSortedFunc(tcs, func(a, b domain.TribeChange) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, tcs[0].ID(), params.Cursor().ID())
for _, tc := range tcs {
assert.GreaterOrEqual(t, tc.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.ListTribeChangesParams {
t.Helper()
params := domain.NewListTribeChangesParams()
require.NoError(t, params.SetSort([]domain.TribeChangeSort{
domain.TribeChangeSortServerKeyDESC,
domain.TribeChangeSortIDDESC,
}))
res, err := repos.tribeChange.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.TribeChanges()), 2)
cursor, err := res.TribeChanges()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
tcs := res.TribeChanges()
assert.NotEmpty(t, len(tcs))
assert.True(t, slices.IsSortedFunc(tcs, func(a, b domain.TribeChange) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, tcs[0].ID(), params.Cursor().ID())
for _, tc := range tcs {
assert.LessOrEqual(t, tc.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.ListTribeChangesParams {
t.Helper()
params := domain.NewListTribeChangesParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(t *testing.T, params domain.ListTribeChangesParams, res domain.ListTribeChangesResult) {
t.Helper()
assert.Len(t, res.TribeChanges(), 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)
},
},
}
@ -317,7 +427,7 @@ func testTribeChangeRepository(t *testing.T, newRepos func(t *testing.T) reposit
res, err := repos.tribeChange.List(ctx, params)
tt.assertError(t, err)
tt.assertTribeChanges(t, params, res)
tt.assertResult(t, params, res)
})
}
})

View File

@ -556,9 +556,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
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()
})))
cursor, err := res.Tribes()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -597,10 +597,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
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()
})))
cursor, err := res.Tribes()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -639,10 +638,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
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()
})))
cursor, err := res.Tribes()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -682,11 +680,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
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()
cfg.Points = res.Tribes()[1].Points()
})))
cursor, err := res.Tribes()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -728,11 +724,9 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
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()
cfg.DeletedAt = res.Tribes()[1].DeletedAt()
})))
cursor, err := res.Tribes()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},

View File

@ -7,7 +7,6 @@ import (
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain/domaintest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -80,9 +79,9 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, err)
require.Greater(t, len(res.Versions()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVersionCursor(t, func(cfg *domaintest.VersionCursorConfig) {
cfg.Code = res.Versions()[1].Code()
})))
cursor, err := res.Versions()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -113,9 +112,9 @@ func testVersionRepository(t *testing.T, newRepos func(t *testing.T) repositorie
require.NoError(t, err)
require.Greater(t, len(res.Versions()), 2)
require.NoError(t, params.SetCursor(domaintest.NewVersionCursor(t, func(cfg *domaintest.VersionCursorConfig) {
cfg.Code = res.Versions()[1].Code()
})))
cursor, err := res.Versions()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},

View File

@ -331,9 +331,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
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()
})))
cursor, err := res.Villages()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -372,10 +372,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
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()
})))
cursor, err := res.Villages()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
@ -414,10 +413,9 @@ func testVillageRepository(t *testing.T, newRepos func(t *testing.T) repositorie
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()
})))
cursor, err := res.Villages()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},

View File

@ -4,9 +4,39 @@ import (
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/require"
)
type TribeChangeCursorConfig struct {
ID int
ServerKey string
CreatedAt time.Time
}
func NewTribeChangeCursor(tb TestingTB, opts ...func(cfg *TribeChangeCursorConfig)) domain.TribeChangeCursor {
tb.Helper()
cfg := &TribeChangeCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
CreatedAt: gofakeit.Date(),
}
for _, opt := range opts {
opt(cfg)
}
tcc, err := domain.NewTribeChangeCursor(
cfg.ID,
cfg.ServerKey,
cfg.CreatedAt,
)
require.NoError(tb, err)
return tcc
}
type TribeChangeConfig struct {
ID int
ServerKey string

View File

@ -81,6 +81,14 @@ func (tc TribeChange) CreatedAt() time.Time {
return tc.createdAt
}
func (tc TribeChange) ToCursor() (TribeChangeCursor, error) {
return NewTribeChangeCursor(tc.id, tc.serverKey, tc.createdAt)
}
func (tc TribeChange) IsZero() bool {
return tc == TribeChange{}
}
type TribeChanges []TribeChange
type CreateTribeChangeParams struct {
@ -241,11 +249,105 @@ func (s TribeChangeSort) String() string {
}
}
type TribeChangeCursor struct {
id int
serverKey string
createdAt time.Time
}
const tribeChangeCursorModelName = "TribeChangeCursor"
func NewTribeChangeCursor(id int, serverKey string, createdAt time.Time) (TribeChangeCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return TribeChangeCursor{}, ValidationError{
Model: tribeChangeCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return TribeChangeCursor{}, ValidationError{
Model: tribeChangeCursorModelName,
Field: "serverKey",
Err: err,
}
}
return TribeChangeCursor{
id: id,
serverKey: serverKey,
createdAt: createdAt,
}, nil
}
//nolint:gocyclo
func decodeTribeChangeCursor(encoded string) (TribeChangeCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return TribeChangeCursor{}, err
}
id, err := m.int("id")
if err != nil {
return TribeChangeCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return TribeChangeCursor{}, ErrInvalidCursor
}
createdAt, err := m.time("createdAt")
if err != nil {
return TribeChangeCursor{}, ErrInvalidCursor
}
ec, err := NewTribeChangeCursor(
id,
serverKey,
createdAt,
)
if err != nil {
return TribeChangeCursor{}, ErrInvalidCursor
}
return ec, nil
}
func (tcc TribeChangeCursor) ID() int {
return tcc.id
}
func (tcc TribeChangeCursor) ServerKey() string {
return tcc.serverKey
}
func (tcc TribeChangeCursor) CreatedAt() time.Time {
return tcc.createdAt
}
func (tcc TribeChangeCursor) IsZero() bool {
return tcc == TribeChangeCursor{}
}
func (tcc TribeChangeCursor) Encode() string {
if tcc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", tcc.id},
{"serverKey", tcc.serverKey},
{"createdAt", tcc.createdAt},
})
}
type ListTribeChangesParams struct {
serverKeys []string
sort []TribeChangeSort
cursor TribeChangeCursor
limit int
offset int
}
const (
@ -308,6 +410,30 @@ func (params *ListTribeChangesParams) SetSort(sort []TribeChangeSort) error {
return nil
}
func (params *ListTribeChangesParams) Cursor() TribeChangeCursor {
return params.cursor
}
func (params *ListTribeChangesParams) SetCursor(cursor TribeChangeCursor) error {
params.cursor = cursor
return nil
}
func (params *ListTribeChangesParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeTribeChangeCursor(encoded)
if err != nil {
return ValidationError{
Model: listTribeChangesParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListTribeChangesParams) Limit() int {
return params.limit
}
@ -326,20 +452,53 @@ func (params *ListTribeChangesParams) SetLimit(limit int) error {
return nil
}
func (params *ListTribeChangesParams) Offset() int {
return params.offset
type ListTribeChangesResult struct {
tribeChanges TribeChanges
self TribeChangeCursor
next TribeChangeCursor
}
func (params *ListTribeChangesParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listTribeChangesParamsModelName,
Field: "offset",
Err: err,
const listTribeChangesResultModelName = "ListTribeChangesResult"
func NewListTribeChangesResult(ennoblements TribeChanges, next TribeChange) (ListTribeChangesResult, error) {
var err error
res := ListTribeChangesResult{
tribeChanges: ennoblements,
}
if len(ennoblements) > 0 {
res.self, err = ennoblements[0].ToCursor()
if err != nil {
return ListTribeChangesResult{}, ValidationError{
Model: listTribeChangesResultModelName,
Field: "self",
Err: err,
}
}
}
params.offset = offset
if !next.IsZero() {
res.next, err = next.ToCursor()
if err != nil {
return ListTribeChangesResult{}, ValidationError{
Model: listTribeChangesResultModelName,
Field: "next",
Err: err,
}
}
}
return nil
return res, nil
}
func (res ListTribeChangesResult) TribeChanges() TribeChanges {
return res.tribeChanges
}
func (res ListTribeChangesResult) Self() TribeChangeCursor {
return res.self
}
func (res ListTribeChangesResult) Next() TribeChangeCursor {
return res.next
}

View File

@ -5,9 +5,11 @@ import (
"fmt"
"slices"
"testing"
"time"
"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"
)
@ -279,6 +281,87 @@ func TestTribeChangeSort_IsInConflict(t *testing.T) {
}
}
func TestNewTribeChangeCursor(t *testing.T) {
t.Parallel()
validTribeChangeCursor := domaintest.NewTribeChangeCursor(t)
type args struct {
id int
serverKey string
createdAt time.Time
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validTribeChangeCursor.ID(),
serverKey: validTribeChangeCursor.ServerKey(),
createdAt: validTribeChangeCursor.CreatedAt(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validTribeChangeCursor.ServerKey(),
createdAt: validTribeChangeCursor.CreatedAt(),
},
expectedErr: domain.ValidationError{
Model: "TribeChangeCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validTribeChangeCursor.ID(),
serverKey: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "TribeChangeCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tcc, err := domain.NewTribeChangeCursor(
tt.args.id,
tt.args.serverKey,
tt.args.createdAt,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, tcc.ID())
assert.Equal(t, tt.args.serverKey, tcc.ServerKey())
assert.Equal(t, tt.args.createdAt, tcc.CreatedAt())
assert.NotEmpty(t, tcc.Encode())
})
}
}
func TestListTribeChangesParams_SetServerKeys(t *testing.T) {
t.Parallel()
@ -422,6 +505,86 @@ func TestListTribeChangesParams_SetSort(t *testing.T) {
}
}
func TestListTribeChangesParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewTribeChangeCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.TribeChangeCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
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: "ListTribeChangesParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListTribeChangesParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListTribeChangesParams()
require.ErrorIs(t, params.SetEncodedCursor(tt.args.cursor), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.cursor, params.Cursor().Encode())
})
}
}
func TestListTribeChangesParams_SetLimit(t *testing.T) {
t.Parallel()
@ -485,51 +648,49 @@ func TestListTribeChangesParams_SetLimit(t *testing.T) {
}
}
func TestListTribeChangesParams_SetOffset(t *testing.T) {
func TestNewListTribeChangesResult(t *testing.T) {
t.Parallel()
type args struct {
offset int
tcs := domain.TribeChanges{
domaintest.NewTribeChange(t),
domaintest.NewTribeChange(t),
domaintest.NewTribeChange(t),
}
next := domaintest.NewTribeChange(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: "ListTribeChangesParams",
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.NewListTribeChangesResult(tcs, next)
require.NoError(t, err)
assert.Equal(t, tcs, res.TribeChanges())
assert.Equal(t, tcs[0].ID(), res.Self().ID())
assert.Equal(t, tcs[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, tcs[0].CreatedAt(), res.Self().CreatedAt())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
assert.Equal(t, next.CreatedAt(), res.Next().CreatedAt())
})
params := domain.NewListTribeChangesParams()
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.NewListTribeChangesResult(tcs, domain.TribeChange{})
require.NoError(t, err)
assert.Equal(t, tcs, res.TribeChanges())
assert.Equal(t, tcs[0].ID(), res.Self().ID())
assert.Equal(t, tcs[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, tcs[0].CreatedAt(), res.Self().CreatedAt())
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 tribe changes", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListTribeChangesResult(nil, domain.TribeChange{})
require.NoError(t, err)
assert.Zero(t, res.TribeChanges())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}

View File

@ -467,16 +467,16 @@ func TestDataSync(t *testing.T) {
allTribeChanges := make(domain.TribeChanges, 0, len(expectedTribeChanges))
for {
tcs, err := tribeChangeRepo.List(ctx, listParams)
res, err := tribeChangeRepo.List(ctx, listParams)
require.NoError(collect, err)
if len(tcs) == 0 {
allTribeChanges = append(allTribeChanges, res.TribeChanges()...)
if res.Next().IsZero() {
break
}
allTribeChanges = append(allTribeChanges, tcs...)
require.NoError(collect, listParams.SetOffset(listParams.Offset()+domain.TribeChangeListMaxLimit))
require.NoError(collect, listParams.SetCursor(res.Next()))
}
if !assert.Len(collect, allTribeChanges, len(expectedTribeChanges)) {