diff --git a/internal/adapter/repository_bun_tribe_change.go b/internal/adapter/repository_bun_tribe_change.go index 5db2eef..5ae09d9 100644 --- a/internal/adapter/repository_bun_tribe_change.go +++ b/internal/adapter/repository_bun_tribe_change.go @@ -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) } diff --git a/internal/adapter/repository_ennoblement_test.go b/internal/adapter/repository_ennoblement_test.go index e45058d..0e6207c 100644 --- a/internal/adapter/repository_ennoblement_test.go +++ b/internal/adapter/repository_ennoblement_test.go @@ -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 }, diff --git a/internal/adapter/repository_player_test.go b/internal/adapter/repository_player_test.go index bc301a7..0db403f 100644 --- a/internal/adapter/repository_player_test.go +++ b/internal/adapter/repository_player_test.go @@ -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 }, diff --git a/internal/adapter/repository_server_test.go b/internal/adapter/repository_server_test.go index d2878df..1262649 100644 --- a/internal/adapter/repository_server_test.go +++ b/internal/adapter/repository_server_test.go @@ -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 }, diff --git a/internal/adapter/repository_test.go b/internal/adapter/repository_test.go index d47dadf..8681efb 100644 --- a/internal/adapter/repository_test.go +++ b/internal/adapter/repository_test.go @@ -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 { diff --git a/internal/adapter/repository_tribe_change_test.go b/internal/adapter/repository_tribe_change_test.go index 606136f..aa6fa27 100644 --- a/internal/adapter/repository_tribe_change_test.go +++ b/internal/adapter/repository_tribe_change_test.go @@ -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) }) } }) diff --git a/internal/adapter/repository_tribe_test.go b/internal/adapter/repository_tribe_test.go index 871589a..637679e 100644 --- a/internal/adapter/repository_tribe_test.go +++ b/internal/adapter/repository_tribe_test.go @@ -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 }, diff --git a/internal/adapter/repository_version_test.go b/internal/adapter/repository_version_test.go index 6b89736..41d1233 100644 --- a/internal/adapter/repository_version_test.go +++ b/internal/adapter/repository_version_test.go @@ -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 }, diff --git a/internal/adapter/repository_village_test.go b/internal/adapter/repository_village_test.go index e86fbe0..29d7a50 100644 --- a/internal/adapter/repository_village_test.go +++ b/internal/adapter/repository_village_test.go @@ -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 }, diff --git a/internal/domain/domaintest/tribe_change.go b/internal/domain/domaintest/tribe_change.go index c2b1e8f..e5b502f 100644 --- a/internal/domain/domaintest/tribe_change.go +++ b/internal/domain/domaintest/tribe_change.go @@ -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 diff --git a/internal/domain/tribe_change.go b/internal/domain/tribe_change.go index 520d513..60cc25c 100644 --- a/internal/domain/tribe_change.go +++ b/internal/domain/tribe_change.go @@ -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 } diff --git a/internal/domain/tribe_change_test.go b/internal/domain/tribe_change_test.go index b0a2f11..1643dde 100644 --- a/internal/domain/tribe_change_test.go +++ b/internal/domain/tribe_change_test.go @@ -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()) + }) } diff --git a/internal/port/consumer_data_sync_test.go b/internal/port/consumer_data_sync_test.go index 4b54d29..08f4743 100644 --- a/internal/port/consumer_data_sync_test.go +++ b/internal/port/consumer_data_sync_test.go @@ -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)) {