feat: player snapshots (#45)

Reviewed-on: twhelp/corev3#45
This commit is contained in:
Dawid Wysokiński 2024-01-16 06:28:03 +00:00
parent 87b3308433
commit ba3b0ea7a6
25 changed files with 1570 additions and 91 deletions

View File

@ -66,6 +66,7 @@ var cmdConsumer = &cli.Command{
c.String(rmqFlagTopicVillagesSyncedEvent.Name),
c.String(rmqFlagTopicEnnoblementsSyncedEvent.Name),
c.String(rmqFlagTopicTribeSnapshotsCreatedEvent.Name),
c.String(rmqFlagTopicPlayerSnapshotsCreatedEvent.Name),
)
consumer.Register(router)
@ -153,6 +154,12 @@ var cmdConsumer = &cli.Command{
marshaler,
c.String(rmqFlagTopicPlayersSyncedEvent.Name),
)
playerSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
newWatermillMarshaler(),
c.String(rmqFlagTopicCreatePlayerSnapshotsCmd.Name),
c.String(rmqFlagTopicPlayerSnapshotsCreatedEvent.Name),
)
twSvc, err := newTWServiceFromFlags(c)
if err != nil {
@ -160,18 +167,26 @@ var cmdConsumer = &cli.Command{
}
tribeChangeSvc := app.NewTribeChangeService(adapter.NewTribeChangeBunRepository(db))
playerSvc := app.NewPlayerService(
adapter.NewPlayerBunRepository(db),
tribeChangeSvc,
twSvc,
playerPublisher,
)
playerSnapshotSvc := app.NewPlayerSnapshotService(
adapter.NewPlayerSnapshotBunRepository(db),
playerSvc,
playerSnapshotPublisher,
)
consumer := port.NewPlayerWatermillConsumer(
app.NewPlayerService(
adapter.NewPlayerBunRepository(db),
tribeChangeSvc,
twSvc,
playerPublisher,
),
playerSvc,
playerSnapshotSvc,
subscriber,
logger,
marshaler,
c.String(rmqFlagTopicServerSyncedEvent.Name),
c.String(rmqFlagTopicCreatePlayerSnapshotsCmd.Name),
)
consumer.Register(router)

View File

@ -198,9 +198,16 @@ var (
c.String(rmqFlagTopicTribeSnapshotsCreatedEvent.Name),
)
playerSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
newWatermillMarshaler(),
c.String(rmqFlagTopicCreatePlayerSnapshotsCmd.Name),
c.String(rmqFlagTopicPlayerSnapshotsCreatedEvent.Name),
)
versionSvc := app.NewVersionService(adapter.NewVersionBunRepository(bunDB))
serverSvc := app.NewServerService(adapter.NewServerBunRepository(bunDB), nil, nil)
snapshotSvc := app.NewSnapshotService(versionSvc, serverSvc, tribeSnapshotPublisher)
snapshotSvc := app.NewSnapshotService(versionSvc, serverSvc, tribeSnapshotPublisher, playerSnapshotPublisher)
shutdownSignalCtx, stop := newShutdownSignalContext(c.Context)
defer stop()

View File

@ -31,11 +31,31 @@ var (
Value: "tribes.event.synced",
EnvVars: []string{"RABBITMQ_TOPIC_TRIBES_SYNCED_EVENT"},
}
rmqFlagTopicCreateTribeSnapshotsCmd = &cli.StringFlag{
Name: "rabbitmq.topic.createTribeSnapshotsCmd",
Value: "tribes.cmd.create_snapshots",
EnvVars: []string{"RABBITMQ_TOPIC_CREATE_TRIBE_SNAPSHOTS_CMD"},
}
rmqFlagTopicTribeSnapshotsCreatedEvent = &cli.StringFlag{
Name: "rabbitmq.topic.tribeSnapshotsCreatedEvent",
Value: "tribes.event.snapshots_created",
EnvVars: []string{"RABBITMQ_TOPIC_TRIBE_SNAPSHOTS_CREATED_EVENT"},
}
rmqFlagTopicPlayersSyncedEvent = &cli.StringFlag{
Name: "rabbitmq.topic.playersSyncedEvent",
Value: "players.event.synced",
EnvVars: []string{"RABBITMQ_TOPIC_PLAYERS_SYNCED_EVENT"},
}
rmqFlagTopicCreatePlayerSnapshotsCmd = &cli.StringFlag{
Name: "rabbitmq.topic.createPlayerSnapshotsCmd",
Value: "players.cmd.create_snapshots",
EnvVars: []string{"RABBITMQ_TOPIC_CREATE_PLAYER_SNAPSHOTS_CMD"},
}
rmqFlagTopicPlayerSnapshotsCreatedEvent = &cli.StringFlag{
Name: "rabbitmq.topic.playerSnapshotsCreatedEvent",
Value: "players.event.snapshots_created",
EnvVars: []string{"RABBITMQ_TOPIC_PLAYER_SNAPSHOTS_CREATED_EVENT"},
}
rmqFlagTopicVillagesSyncedEvent = &cli.StringFlag{
Name: "rabbitmq.topic.villagesSyncedEvent",
Value: "villages.event.synced",
@ -51,27 +71,19 @@ var (
Value: "ennoblements.event.synced",
EnvVars: []string{"RABBITMQ_TOPIC_ENNOBLEMENTS_SYNCED_EVENT"},
}
rmqFlagTopicCreateTribeSnapshotsCmd = &cli.StringFlag{
Name: "rabbitmq.topic.createTribeSnapshotsCmd",
Value: "tribes.cmd.create_snapshots",
EnvVars: []string{"RABBITMQ_TOPIC_CREATE_TRIBE_SNAPSHOTS_CMD"},
}
rmqFlagTopicTribeSnapshotsCreatedEvent = &cli.StringFlag{
Name: "rabbitmq.topic.tribeSnapshotsCreatedEvent",
Value: "tribes.event.snapshots_created",
EnvVars: []string{"RABBITMQ_TOPIC_TRIBE_SNAPSHOTS_CREATED_EVENT"},
}
rmqFlags = []cli.Flag{
rmqFlagConnectionString,
rmqFlagTopicSyncServersCmd,
rmqFlagTopicServerSyncedEvent,
rmqFlagTopicTribesSyncedEvent,
rmqFlagTopicCreateTribeSnapshotsCmd,
rmqFlagTopicTribeSnapshotsCreatedEvent,
rmqFlagTopicPlayersSyncedEvent,
rmqFlagTopicCreatePlayerSnapshotsCmd,
rmqFlagTopicPlayerSnapshotsCreatedEvent,
rmqFlagTopicVillagesSyncedEvent,
rmqFlagTopicSyncEnnoblementsCmd,
rmqFlagTopicEnnoblementsSyncedEvent,
rmqFlagTopicCreateTribeSnapshotsCmd,
rmqFlagTopicTribeSnapshotsCreatedEvent,
}
)

View File

@ -0,0 +1,105 @@
package adapter
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/bun/bunmodel"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/uptrace/bun"
)
type PlayerSnapshotBunRepository struct {
db bun.IDB
}
func NewPlayerSnapshotBunRepository(db bun.IDB) *PlayerSnapshotBunRepository {
return &PlayerSnapshotBunRepository{db: db}
}
func (repo *PlayerSnapshotBunRepository) Create(
ctx context.Context,
params ...domain.CreatePlayerSnapshotParams,
) error {
if len(params) == 0 {
return nil
}
now := time.Now()
playerSnapshots := make(bunmodel.PlayerSnapshots, 0, len(params))
for _, p := range params {
playerSnapshots = append(playerSnapshots, bunmodel.PlayerSnapshot{
PlayerID: p.PlayerID(),
NumVillages: p.NumVillages(),
Points: p.Points(),
Rank: p.Rank(),
TribeID: p.TribeID(),
ServerKey: p.ServerKey(),
Date: p.Date(),
CreatedAt: now,
OpponentsDefeated: bunmodel.NewOpponentsDefeated(p.OD()),
})
}
if _, err := repo.db.NewInsert().
Model(&playerSnapshots).
Ignore().
Returning("").
Exec(ctx); err != nil {
return fmt.Errorf("something went wrong while inserting player snapshots into the db: %w", err)
}
return nil
}
func (repo *PlayerSnapshotBunRepository) List(
ctx context.Context,
params domain.ListPlayerSnapshotsParams,
) (domain.PlayerSnapshots, error) {
var playerSnapshots bunmodel.PlayerSnapshots
if err := repo.db.NewSelect().
Model(&playerSnapshots).
Apply(listPlayerSnapshotsParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("couldn't select player snapshots from the db: %w", err)
}
return playerSnapshots.ToDomain()
}
type listPlayerSnapshotsParamsApplier struct {
params domain.ListPlayerSnapshotsParams
}
//nolint:gocyclo
func (a listPlayerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("ps.server_key IN (?)", bun.In(serverKeys))
}
for _, s := range a.params.Sort() {
switch s {
case domain.PlayerSnapshotSortDateASC:
q = q.Order("ps.date ASC")
case domain.PlayerSnapshotSortDateDESC:
q = q.Order("ps.date DESC")
case domain.PlayerSnapshotSortIDASC:
q = q.Order("ps.id ASC")
case domain.PlayerSnapshotSortIDDESC:
q = q.Order("ps.id DESC")
case domain.PlayerSnapshotSortServerKeyASC:
q = q.Order("ps.server_key ASC")
case domain.PlayerSnapshotSortServerKeyDESC:
q = q.Order("ps.server_key DESC")
default:
return q.Err(errors.New("unsupported sort value"))
}
}
return q.Limit(a.params.Limit()).Offset(a.params.Offset())
}

View File

@ -0,0 +1,29 @@
package adapter_test
import (
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/bun/buntest"
)
func TestPlayerSnapshotBunRepository_Postgres(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping long-running test")
}
testPlayerSnapshotRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, postgres.NewDB(t))
})
}
func TestPlayerSnapshotBunRepository_SQLite(t *testing.T) {
t.Parallel()
testPlayerSnapshotRepository(t, func(t *testing.T) repositories {
t.Helper()
return newBunDBRepositories(t, buntest.NewSQLiteDB(t))
})
}

View File

@ -167,6 +167,10 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
q = q.Set("tribe_snapshots_created_at = ?", tribeSnapshotsCreatedAt.Value)
}
if playerSnapshotsCreatedAt := a.params.PlayerSnapshotsCreatedAt(); playerSnapshotsCreatedAt.Valid {
q = q.Set("player_snapshots_created_at = ?", playerSnapshotsCreatedAt.Value)
}
return q
}

View File

@ -0,0 +1,335 @@
package adapter_test
import (
"cmp"
"context"
"fmt"
"slices"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repositories) {
t.Helper()
ctx := context.Background()
t.Run("Create", func(t *testing.T) {
t.Parallel()
const dateFormat = "2006-01-02"
repos := newRepos(t)
assertCreated := func(t *testing.T, params []domain.CreatePlayerSnapshotParams) {
t.Helper()
require.NotEmpty(t, params)
playerSnapshots, err := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams())
require.NoError(t, err)
for i, p := range params {
date := p.Date().Format(dateFormat)
idx := slices.IndexFunc(playerSnapshots, func(ps domain.PlayerSnapshot) bool {
return ps.ServerKey() == p.ServerKey() &&
ps.PlayerID() == p.PlayerID() &&
ps.Date().Format(dateFormat) == date
})
require.GreaterOrEqualf(t, idx, 0, "params[%d] not found", i)
playerSnapshot := playerSnapshots[idx]
assert.Equalf(t, p.PlayerID(), playerSnapshot.PlayerID(), "params[%d]", i)
assert.Equalf(t, p.ServerKey(), playerSnapshot.ServerKey(), "params[%d]", i)
assert.Equalf(t, p.NumVillages(), playerSnapshot.NumVillages(), "params[%d]", i)
assert.Equalf(t, p.Points(), playerSnapshot.Points(), "params[%d]", i)
assert.Equalf(t, p.Rank(), playerSnapshot.Rank(), "params[%d]", i)
assert.Equalf(t, p.TribeID(), playerSnapshot.TribeID(), "params[%d]", i)
assert.Equalf(t, p.OD(), playerSnapshot.OD(), "params[%d]", i)
assert.Equalf(t, date, playerSnapshot.Date().Format(dateFormat), "params[%d]", i)
assert.WithinDurationf(t, time.Now(), playerSnapshot.CreatedAt(), time.Minute, "params[%d]", i)
}
}
assertNoDuplicates := func(t *testing.T, params []domain.CreatePlayerSnapshotParams) {
t.Helper()
require.NotEmpty(t, params)
playerSnapshots, err := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams())
require.NoError(t, err)
res := make(map[string][]int)
for _, p := range params {
key := fmt.Sprintf("%s-%d-%s", p.ServerKey(), p.PlayerID(), p.Date().Format(dateFormat))
for i, ps := range playerSnapshots {
if ps.ServerKey() == p.ServerKey() && ps.PlayerID() == p.PlayerID() && ps.Date().Equal(p.Date()) {
res[key] = append(res[key], i)
}
}
}
for key, indexes := range res {
assert.Len(t, indexes, 1, key)
}
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
listPlayersParams := domain.NewListPlayersParams()
require.NoError(t, listPlayersParams.SetDeleted(domain.NullBool{
Value: false,
Valid: true,
}))
require.NoError(t, listPlayersParams.SetLimit(domain.PlayerSnapshotListMaxLimit/2))
players, err := repos.player.List(ctx, listPlayersParams)
require.NoError(t, err)
require.NotEmpty(t, players)
date := time.Now()
createParams, err := domain.NewCreatePlayerSnapshotParams(players, date)
require.NoError(t, err)
require.NoError(t, repos.playerSnapshot.Create(ctx, createParams...))
assertCreated(t, createParams)
require.NoError(t, repos.playerSnapshot.Create(ctx, createParams...))
assertNoDuplicates(t, createParams)
})
t.Run("OK: len(params) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.playerSnapshot.Create(ctx))
})
})
t.Run("List & ListCount", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
playerSnapshots, listPlayerSnapshotsErr := repos.playerSnapshot.List(ctx, domain.NewListPlayerSnapshotsParams())
require.NoError(t, listPlayerSnapshotsErr)
require.NotEmpty(t, playerSnapshots)
randPlayerSnapshot := playerSnapshots[0]
tests := []struct {
name string
params func(t *testing.T) domain.ListPlayerSnapshotsParams
assertPlayerSnapshots func(
t *testing.T,
params domain.ListPlayerSnapshotsParams,
playerSnapshots domain.PlayerSnapshots,
)
assertError func(t *testing.T, err error)
assertTotal func(t *testing.T, params domain.ListPlayerSnapshotsParams, total int)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
return domain.NewListPlayerSnapshotsParams()
},
assertPlayerSnapshots: func(
t *testing.T,
params domain.ListPlayerSnapshotsParams,
playerSnapshots domain.PlayerSnapshots,
) {
t.Helper()
assert.NotEmpty(t, len(playerSnapshots))
assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int {
if x := cmp.Compare(a.ServerKey(), b.ServerKey()); x != 0 {
return x
}
if x := a.Date().Compare(b.Date()); x != 0 {
return x
}
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, params domain.ListPlayerSnapshotsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[serverKey DESC, date DESC]",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortServerKeyDESC,
domain.PlayerSnapshotSortDateDESC,
}))
return params
},
assertPlayerSnapshots: func(
t *testing.T,
params domain.ListPlayerSnapshotsParams,
playerSnapshots domain.PlayerSnapshots,
) {
t.Helper()
assert.NotEmpty(t, len(playerSnapshots))
assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int {
if x := cmp.Compare(a.ServerKey(), b.ServerKey()) * -1; x != 0 {
return x
}
return a.Date().Compare(b.Date()) * -1
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListPlayerSnapshotsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[id ASC]",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortIDASC,
}))
return params
},
assertPlayerSnapshots: func(
t *testing.T,
params domain.ListPlayerSnapshotsParams,
playerSnapshots domain.PlayerSnapshots,
) {
t.Helper()
assert.NotEmpty(t, len(playerSnapshots))
assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) 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, params domain.ListPlayerSnapshotsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: sort=[id DESC]",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortIDDESC,
}))
return params
},
assertPlayerSnapshots: func(
t *testing.T,
params domain.ListPlayerSnapshotsParams,
playerSnapshots domain.PlayerSnapshots,
) {
t.Helper()
assert.NotEmpty(t, len(playerSnapshots))
assert.True(t, slices.IsSortedFunc(playerSnapshots, func(a, b domain.PlayerSnapshot) int {
return cmp.Compare(a.ID(), b.ID()) * -1
}))
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListPlayerSnapshotsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: fmt.Sprintf("OK: serverKeys=[%s]", randPlayerSnapshot.ServerKey()),
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
require.NoError(t, params.SetServerKeys([]string{randPlayerSnapshot.ServerKey()}))
return params
},
assertPlayerSnapshots: func(
t *testing.T,
params domain.ListPlayerSnapshotsParams,
playerSnapshots domain.PlayerSnapshots,
) {
t.Helper()
serverKeys := params.ServerKeys()
for _, ps := range playerSnapshots {
assert.True(t, slices.Contains(serverKeys, ps.ServerKey()))
}
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListPlayerSnapshotsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
{
name: "OK: offset=1 limit=2",
params: func(t *testing.T) domain.ListPlayerSnapshotsParams {
t.Helper()
params := domain.NewListPlayerSnapshotsParams()
require.NoError(t, params.SetOffset(1))
require.NoError(t, params.SetLimit(2))
return params
},
assertPlayerSnapshots: func(
t *testing.T,
params domain.ListPlayerSnapshotsParams,
playerSnapshots domain.PlayerSnapshots,
) {
t.Helper()
assert.Len(t, playerSnapshots, params.Limit())
},
assertError: func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
},
assertTotal: func(t *testing.T, params domain.ListPlayerSnapshotsParams, total int) {
t.Helper()
assert.NotEmpty(t, total)
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := tt.params(t)
res, err := repos.playerSnapshot.List(ctx, params)
tt.assertError(t, err)
tt.assertPlayerSnapshots(t, params, res)
})
}
})
}

View File

@ -512,6 +512,10 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
Value: time.Now(),
Valid: true,
}))
require.NoError(t, updateParams.SetPlayerSnapshotsCreatedAt(domain.NullTime{
Value: time.Now(),
Valid: true,
}))
require.NoError(t, repos.server.Update(ctx, serversBeforeUpdate[0].Key(), updateParams))
@ -561,6 +565,12 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
serversAfterUpdate[0].TribeSnapshotsCreatedAt(),
time.Minute,
)
assert.WithinDuration(
t,
updateParams.PlayerSnapshotsCreatedAt().Value,
serversAfterUpdate[0].PlayerSnapshotsCreatedAt(),
time.Minute,
)
})
t.Run("ERR: not found", func(t *testing.T) {

View File

@ -59,15 +59,21 @@ type tribeSnapshotRepository interface {
List(ctx context.Context, params domain.ListTribeSnapshotsParams) (domain.TribeSnapshots, error)
}
type playerSnapshotRepository interface {
Create(ctx context.Context, params ...domain.CreatePlayerSnapshotParams) error
List(ctx context.Context, params domain.ListPlayerSnapshotsParams) (domain.PlayerSnapshots, error)
}
type repositories struct {
version versionRepository
server serverRepository
tribe tribeRepository
player playerRepository
village villageRepository
ennoblement ennoblementRepository
tribeChange tribeChangeRepository
tribeSnapshot tribeSnapshotRepository
version versionRepository
server serverRepository
tribe tribeRepository
player playerRepository
village villageRepository
ennoblement ennoblementRepository
tribeChange tribeChangeRepository
tribeSnapshot tribeSnapshotRepository
playerSnapshot playerSnapshotRepository
}
func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
@ -76,13 +82,14 @@ func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
buntest.NewFixture(bunDB).Load(tb, context.Background(), os.DirFS("testdata"), "fixture.yml")
return repositories{
version: adapter.NewVersionBunRepository(bunDB),
server: adapter.NewServerBunRepository(bunDB),
tribe: adapter.NewTribeBunRepository(bunDB),
player: adapter.NewPlayerBunRepository(bunDB),
village: adapter.NewVillageBunRepository(bunDB),
ennoblement: adapter.NewEnnoblementBunRepository(bunDB),
tribeChange: adapter.NewTribeChangeBunRepository(bunDB),
tribeSnapshot: adapter.NewTribeSnapshotBunRepository(bunDB),
version: adapter.NewVersionBunRepository(bunDB),
server: adapter.NewServerBunRepository(bunDB),
tribe: adapter.NewTribeBunRepository(bunDB),
player: adapter.NewPlayerBunRepository(bunDB),
village: adapter.NewVillageBunRepository(bunDB),
ennoblement: adapter.NewEnnoblementBunRepository(bunDB),
tribeChange: adapter.NewTribeChangeBunRepository(bunDB),
tribeSnapshot: adapter.NewTribeSnapshotBunRepository(bunDB),
playerSnapshot: adapter.NewPlayerSnapshotBunRepository(bunDB),
}
}

View File

@ -7496,3 +7496,95 @@
dominance: 54.27478896611433
date: 2020-06-23T00:00:00.000Z
created_at: 2020-06-23T13:45:46.000Z
- model: PlayerSnapshot
rows:
- rank_att: 5
score_att: 38213157
rank_def: 2
score_def: 51137731
rank_sup: 104
score_sup: 2282683
rank_total: 1
score_total: 91633571
_id: pl169-chcesz-remont-2021-09-02
id: 100000
player_id: 6180190
server_key: pl169
num_villages: 665
points: 7298055
rank: 1
tribe_id: 27
date: 2021-09-02T00:00:00.000Z
created_at: 2021-09-02T21:01:03.000Z
- rank_att: 5
score_att: 38213157
rank_def: 2
score_def: 51137731
rank_sup: 104
score_sup: 2282683
rank_total: 1
score_total: 91633571
_id: pl169-chcesz-remont-2021-09-03
id: 100001
player_id: 6180190
server_key: pl169
num_villages: 665
points: 7298055
rank: 1
tribe_id: 27
date: 2021-09-03T00:00:00.000Z
created_at: 2021-09-03T21:01:03.000Z
- rank_att: 13
score_att: 23124653
rank_def: 112
score_def: 5547007
rank_sup: 10
score_sup: 10651344
rank_total: 23
score_total: 39323004
_id: pl169-maddov-2021-09-08
id: 100050
player_id: 8419570
server_key: pl169
num_villages: 643
points: 6292398
rank: 2
tribe_id: 2
date: 2021-09-08T00:00:00.000Z
created_at: 2021-09-08T20:01:11.000Z
- rank_att: 10
score_att: 27638462
rank_def: 35
score_def: 5555868
rank_sup: 3
score_sup: 9815277
rank_total: 16
score_total: 43009607
_id: de188-1scoo-2021-06-25
id: 100100
player_id: 1577279214
server_key: de188
num_villages: 878
points: 9199775
rank: 1
tribe_id: 772
date: 2021-06-25T00:00:00.000Z
created_at: 2021-06-25T11:00:53.000Z
- rank_att: 10
score_att: 27638462
rank_def: 35
score_def: 5555868
rank_sup: 3
score_sup: 9815277
rank_total: 16
score_total: 43009607
_id: de188-1scoo-2021-06-26
id: 100101
player_id: 1577279214
server_key: de188
num_villages: 878
points: 9199775
rank: 1
tribe_id: 772
date: 2021-06-26T00:00:00.000Z
created_at: 2021-06-26T11:00:53.000Z

View File

@ -180,3 +180,7 @@ func (svc *PlayerService) delete(ctx context.Context, serverKey string, players
return svc.tribeChangeSvc.Create(ctx, tribeChangesParams...)
}
func (svc *PlayerService) List(ctx context.Context, params domain.ListPlayersParams) (domain.Players, error) {
return svc.repo.List(ctx, params)
}

View File

@ -0,0 +1,92 @@
package app
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
)
type PlayerSnapshotRepository interface {
// Create persists player snapshots in a store (e.g. Postgres).
// Duplicates are ignored.
Create(ctx context.Context, params ...domain.CreatePlayerSnapshotParams) error
}
type PlayerSnapshotService struct {
repo PlayerSnapshotRepository
playerSvc *PlayerService
pub SnapshotPublisher
}
func NewPlayerSnapshotService(
repo PlayerSnapshotRepository,
playerSvc *PlayerService,
pub SnapshotPublisher,
) *PlayerSnapshotService {
return &PlayerSnapshotService{repo: repo, playerSvc: playerSvc, pub: pub}
}
//nolint:gocyclo
func (svc *PlayerSnapshotService) Create(
ctx context.Context,
createSnapshotsCmdPayload domain.CreateSnapshotsCmdPayload,
) error {
serverKey := createSnapshotsCmdPayload.ServerKey()
date := createSnapshotsCmdPayload.Date()
listPlayersParams := domain.NewListPlayersParams()
if err := listPlayersParams.SetServerKeys([]string{serverKey}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if err := listPlayersParams.SetDeleted(domain.NullBool{
Value: false,
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if err := listPlayersParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if err := listPlayersParams.SetLimit(domain.PlayerListMaxLimit); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
for {
players, err := svc.playerSvc.List(ctx, listPlayersParams)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if len(players) == 0 {
break
}
params, err := domain.NewCreatePlayerSnapshotParams(players, date)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if err = svc.repo.Create(ctx, params...); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if err = listPlayersParams.SetIDGT(domain.NullInt{
Value: players[len(players)-1].ID(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
}
payload, err := domain.NewSnapshotsCreatedEventPayload(serverKey, createSnapshotsCmdPayload.VersionCode())
if err != nil {
return fmt.Errorf("%s: couldn't construct domain.SnapshotsCreatedEventPayload: %w", serverKey, err)
}
if err = svc.pub.EventCreated(ctx, payload); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
return nil
}

View File

@ -289,3 +289,20 @@ func (svc *ServerService) UpdateTribeSnapshotsCreatedAt(
return svc.repo.Update(ctx, key, updateParams)
}
func (svc *ServerService) UpdatePlayerSnapshotsCreatedAt(
ctx context.Context,
payload domain.SnapshotsCreatedEventPayload,
) error {
key := payload.ServerKey()
var updateParams domain.UpdateServerParams
if err := updateParams.SetPlayerSnapshotsCreatedAt(domain.NullTime{
Value: time.Now(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
return svc.repo.Update(ctx, key, updateParams)
}

View File

@ -2,6 +2,7 @@ package app
import (
"context"
"errors"
"fmt"
"time"
@ -9,20 +10,23 @@ import (
)
type SnapshotService struct {
versionSvc *VersionService
serverSvc *ServerService
tribeSnapshotPublisher SnapshotPublisher
versionSvc *VersionService
serverSvc *ServerService
tribeSnapshotPublisher SnapshotPublisher
playerSnapshotPublisher SnapshotPublisher
}
func NewSnapshotService(
versionSvc *VersionService,
serverSvc *ServerService,
tribeSnapshotPublisher SnapshotPublisher,
playerSnapshotPublisher SnapshotPublisher,
) *SnapshotService {
return &SnapshotService{
versionSvc: versionSvc,
serverSvc: serverSvc,
tribeSnapshotPublisher: tribeSnapshotPublisher,
versionSvc: versionSvc,
serverSvc: serverSvc,
tribeSnapshotPublisher: tribeSnapshotPublisher,
playerSnapshotPublisher: playerSnapshotPublisher,
}
}
@ -42,7 +46,11 @@ func (svc *SnapshotService) Create(ctx context.Context) error {
snapshotsCreatedAtLT := time.Date(year, month, day, 0, 0, 0, 0, loc)
date := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
if loopErr = svc.createTribe(ctx, v, snapshotsCreatedAtLT, date); loopErr != nil {
loopErr = errors.Join(
svc.publishTribe(ctx, v, snapshotsCreatedAtLT, date),
svc.publishPlayer(ctx, v, snapshotsCreatedAtLT, date),
)
if loopErr != nil {
return loopErr
}
}
@ -50,7 +58,7 @@ func (svc *SnapshotService) Create(ctx context.Context) error {
return nil
}
func (svc *SnapshotService) createTribe(
func (svc *SnapshotService) publishTribe(
ctx context.Context,
v domain.Version,
snapshotsCreatedAtLT time.Time,
@ -86,6 +94,42 @@ func (svc *SnapshotService) createTribe(
return svc.tribeSnapshotPublisher.CmdCreate(ctx, payloads...)
}
func (svc *SnapshotService) publishPlayer(
ctx context.Context,
v domain.Version,
snapshotsCreatedAtLT time.Time,
date time.Time,
) error {
params := domain.NewListServersParams()
if err := params.SetVersionCodes([]string{v.Code()}); err != nil {
return err
}
if err := params.SetOpen(domain.NullBool{
Value: true,
Valid: true,
}); err != nil {
return err
}
if err := params.SetPlayerSnapshotsCreatedAtLT(domain.NullTime{
Value: snapshotsCreatedAtLT,
Valid: true,
}); err != nil {
return err
}
servers, err := svc.serverSvc.ListAll(ctx, params)
if err != nil {
return err
}
payloads, err := svc.toPayload(v, servers, date)
if err != nil {
return err
}
return svc.playerSnapshotPublisher.CmdCreate(ctx, payloads...)
}
func (svc *SnapshotService) toPayload(
v domain.Version,
servers domain.Servers,

View File

@ -0,0 +1,76 @@
package bunmodel
import (
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/domain"
"github.com/uptrace/bun"
)
type PlayerSnapshot struct {
bun.BaseModel `bun:"table:player_snapshots,alias:ps"`
ID int `bun:"id,pk,autoincrement,identity"`
PlayerID int `bun:"player_id,nullzero"`
NumVillages int `bun:"num_villages"`
Points int `bun:"points"`
Rank int `bun:"rank"`
TribeID int `bun:"tribe_id,nullzero"`
Tribe Tribe `bun:"tribe,rel:belongs-to,join:tribe_id=id,join:server_key=server_key"`
ServerKey string `bun:"server_key,nullzero"`
Date time.Time `bun:"date,nullzero"`
CreatedAt time.Time `bun:"created_at,nullzero"`
OpponentsDefeated
}
func (ps PlayerSnapshot) ToDomain() (domain.PlayerSnapshot, error) {
od, err := ps.OpponentsDefeated.ToDomain()
if err != nil {
return domain.PlayerSnapshot{}, fmt.Errorf(
"couldn't construct domain.PlayerSnapshot (id=%d): %w",
ps.ID,
err,
)
}
converted, err := domain.UnmarshalPlayerSnapshotFromDatabase(
ps.ID,
ps.PlayerID,
ps.ServerKey,
ps.NumVillages,
ps.Points,
ps.Rank,
ps.TribeID,
od,
ps.Date,
ps.CreatedAt,
)
if err != nil {
return domain.PlayerSnapshot{}, fmt.Errorf(
"couldn't construct domain.PlayerSnapshot (id=%d): %w",
ps.ID,
err,
)
}
return converted, nil
}
type PlayerSnapshots []PlayerSnapshot
func (pss PlayerSnapshots) ToDomain() (domain.PlayerSnapshots, error) {
res := make(domain.PlayerSnapshots, 0, len(pss))
for _, ps := range pss {
converted, err := ps.ToDomain()
if err != nil {
return nil, err
}
res = append(res, converted)
}
return res, nil
}

View File

@ -24,6 +24,7 @@ func NewFixture(bunDB *bun.DB) *Fixture {
(*bunmodel.Ennoblement)(nil),
(*bunmodel.TribeChange)(nil),
(*bunmodel.TribeSnapshot)(nil),
(*bunmodel.PlayerSnapshot)(nil),
)
return &Fixture{
f: dbfixture.New(bunDB),

View File

@ -197,6 +197,10 @@ func (p Player) IsDeleted() bool {
return !p.deletedAt.IsZero()
}
func (p Player) IsZero() bool {
return p == Player{}
}
type Players []Player
// Delete finds all players with the given serverKey that are not in the given slice with active players

View File

@ -0,0 +1,289 @@
package domain
import (
"fmt"
"math"
"time"
)
type PlayerSnapshot struct {
id int
playerID int
serverKey string
numVillages int
points int
rank int
tribeID int
od OpponentsDefeated
date time.Time
createdAt time.Time
}
const playerSnapshotModelName = "PlayerSnapshot"
// UnmarshalPlayerSnapshotFromDatabase unmarshals PlayerSnapshot from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalPlayerSnapshotFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalPlayerSnapshotFromDatabase(
id int,
playerID int,
serverKey string,
numVillages int,
points int,
rank int,
tribeID int,
od OpponentsDefeated,
date time.Time,
createdAt time.Time,
) (PlayerSnapshot, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return PlayerSnapshot{}, ValidationError{
Model: playerSnapshotModelName,
Field: "id",
Err: err,
}
}
if err := validateIntInRange(playerID, 1, math.MaxInt); err != nil {
return PlayerSnapshot{}, ValidationError{
Model: playerSnapshotModelName,
Field: "playerID",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return PlayerSnapshot{}, ValidationError{
Model: playerSnapshotModelName,
Field: "serverKey",
Err: err,
}
}
return PlayerSnapshot{
id: id,
playerID: playerID,
serverKey: serverKey,
numVillages: numVillages,
points: points,
rank: rank,
tribeID: tribeID,
od: od,
date: date,
createdAt: createdAt,
}, nil
}
func (ps PlayerSnapshot) ID() int {
return ps.id
}
func (ps PlayerSnapshot) PlayerID() int {
return ps.playerID
}
func (ps PlayerSnapshot) ServerKey() string {
return ps.serverKey
}
func (ps PlayerSnapshot) NumVillages() int {
return ps.numVillages
}
func (ps PlayerSnapshot) Points() int {
return ps.points
}
func (ps PlayerSnapshot) Rank() int {
return ps.rank
}
func (ps PlayerSnapshot) TribeID() int {
return ps.tribeID
}
func (ps PlayerSnapshot) OD() OpponentsDefeated {
return ps.od
}
func (ps PlayerSnapshot) Date() time.Time {
return ps.date
}
func (ps PlayerSnapshot) CreatedAt() time.Time {
return ps.createdAt
}
type PlayerSnapshots []PlayerSnapshot
type CreatePlayerSnapshotParams struct {
playerID int
serverKey string
numVillages int
points int
rank int
tribeID int
od OpponentsDefeated
date time.Time
}
func NewCreatePlayerSnapshotParams(players Players, date time.Time) ([]CreatePlayerSnapshotParams, error) {
params := make([]CreatePlayerSnapshotParams, 0, len(players))
for i, p := range players {
if p.IsZero() {
return nil, fmt.Errorf("players[%d] is an empty struct", i)
}
if p.IsDeleted() {
continue
}
params = append(params, CreatePlayerSnapshotParams{
playerID: p.ID(),
serverKey: p.ServerKey(),
numVillages: p.NumVillages(),
points: p.Points(),
rank: p.Rank(),
tribeID: p.TribeID(),
od: p.OD(),
date: date,
})
}
return params, nil
}
func (params CreatePlayerSnapshotParams) PlayerID() int {
return params.playerID
}
func (params CreatePlayerSnapshotParams) ServerKey() string {
return params.serverKey
}
func (params CreatePlayerSnapshotParams) NumVillages() int {
return params.numVillages
}
func (params CreatePlayerSnapshotParams) Points() int {
return params.points
}
func (params CreatePlayerSnapshotParams) Rank() int {
return params.rank
}
func (params CreatePlayerSnapshotParams) TribeID() int {
return params.tribeID
}
func (params CreatePlayerSnapshotParams) OD() OpponentsDefeated {
return params.od
}
func (params CreatePlayerSnapshotParams) Date() time.Time {
return params.date
}
type PlayerSnapshotSort uint8
const (
PlayerSnapshotSortDateASC PlayerSnapshotSort = iota + 1
PlayerSnapshotSortDateDESC
PlayerSnapshotSortIDASC
PlayerSnapshotSortIDDESC
PlayerSnapshotSortServerKeyASC
PlayerSnapshotSortServerKeyDESC
)
const PlayerSnapshotListMaxLimit = 200
type ListPlayerSnapshotsParams struct {
serverKeys []string
sort []PlayerSnapshotSort
limit int
offset int
}
const listPlayerSnapshotsParamsModelName = "ListPlayerSnapshotsParams"
func NewListPlayerSnapshotsParams() ListPlayerSnapshotsParams {
return ListPlayerSnapshotsParams{
sort: []PlayerSnapshotSort{
PlayerSnapshotSortServerKeyASC,
PlayerSnapshotSortDateASC,
PlayerSnapshotSortIDASC,
},
limit: PlayerSnapshotListMaxLimit,
}
}
func (params *ListPlayerSnapshotsParams) ServerKeys() []string {
return params.serverKeys
}
func (params *ListPlayerSnapshotsParams) SetServerKeys(serverKeys []string) error {
params.serverKeys = serverKeys
return nil
}
func (params *ListPlayerSnapshotsParams) Sort() []PlayerSnapshotSort {
return params.sort
}
const (
playerSnapshotSortMinLength = 1
playerSnapshotSortMaxLength = 3
)
func (params *ListPlayerSnapshotsParams) SetSort(sort []PlayerSnapshotSort) error {
if err := validateSliceLen(sort, playerSnapshotSortMinLength, playerSnapshotSortMaxLength); err != nil {
return ValidationError{
Model: listPlayerSnapshotsParamsModelName,
Field: "sort",
Err: err,
}
}
params.sort = sort
return nil
}
func (params *ListPlayerSnapshotsParams) Limit() int {
return params.limit
}
func (params *ListPlayerSnapshotsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, PlayerSnapshotListMaxLimit); err != nil {
return ValidationError{
Model: listPlayerSnapshotsParamsModelName,
Field: "limit",
Err: err,
}
}
params.limit = limit
return nil
}
func (params *ListPlayerSnapshotsParams) Offset() int {
return params.offset
}
func (params *ListPlayerSnapshotsParams) SetOffset(offset int) error {
if err := validateIntInRange(offset, 0, math.MaxInt); err != nil {
return ValidationError{
Model: listPlayerSnapshotsParamsModelName,
Field: "offset",
Err: err,
}
}
params.offset = offset
return nil
}

View File

@ -0,0 +1,236 @@
package domain_test
import (
"fmt"
"slices"
"testing"
"time"
"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"
)
func TestNewCreatePlayerSnapshotParams(t *testing.T) {
t.Parallel()
players := domain.Players{
domaintest.NewPlayer(t),
domaintest.NewPlayer(t),
domaintest.NewPlayer(t),
}
date := time.Now()
res, err := domain.NewCreatePlayerSnapshotParams(players, date)
require.NoError(t, err)
assert.Len(t, res, len(players))
for i, p := range players {
idx := slices.IndexFunc(res, func(params domain.CreatePlayerSnapshotParams) bool {
return params.PlayerID() == p.ID() && params.ServerKey() == p.ServerKey()
})
require.GreaterOrEqualf(t, idx, 0, "players[%d] not found", i)
params := res[i]
assert.Equalf(t, p.ID(), params.PlayerID(), "players[%d]", i)
assert.Equalf(t, p.ServerKey(), params.ServerKey(), "players[%d]", i)
assert.Equalf(t, p.NumVillages(), params.NumVillages(), "players[%d]", i)
assert.Equalf(t, p.Points(), params.Points(), "players[%d]", i)
assert.Equalf(t, p.Rank(), params.Rank(), "players[%d]", i)
assert.Equalf(t, p.TribeID(), params.TribeID(), "players[%d]", i)
assert.Equalf(t, p.OD(), params.OD(), "players[%d]", i)
assert.Equalf(t, date, params.Date(), "players[%d]", i)
}
}
func TestListPlayerSnapshotsParams_SetSort(t *testing.T) {
t.Parallel()
type args struct {
sort []domain.PlayerSnapshotSort
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortDateASC,
domain.PlayerSnapshotSortServerKeyASC,
},
},
},
{
name: "ERR: len(sort) < 1",
args: args{
sort: nil,
},
expectedErr: domain.ValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 3,
Current: 0,
},
},
},
{
name: "ERR: len(sort) > 3",
args: args{
sort: []domain.PlayerSnapshotSort{
domain.PlayerSnapshotSortDateASC,
domain.PlayerSnapshotSortServerKeyASC,
domain.PlayerSnapshotSortIDASC,
domain.PlayerSnapshotSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 3,
Current: 4,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListPlayerSnapshotsParams()
require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.sort, params.Sort())
})
}
}
func TestListPlayerSnapshotsParams_SetLimit(t *testing.T) {
t.Parallel()
type args struct {
limit int
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
limit: domain.PlayerSnapshotListMaxLimit,
},
},
{
name: "ERR: limit < 1",
args: args{
limit: 0,
},
expectedErr: domain.ValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.PlayerSnapshotListMaxLimit),
args: args{
limit: domain.PlayerSnapshotListMaxLimit + 1,
},
expectedErr: domain.ValidationError{
Model: "ListPlayerSnapshotsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Max: domain.PlayerSnapshotListMaxLimit,
Current: domain.PlayerSnapshotListMaxLimit + 1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListPlayerSnapshotsParams()
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.limit, params.Limit())
})
}
}
func TestListPlayerSnapshotsParams_SetOffset(t *testing.T) {
t.Parallel()
type args struct {
offset int
}
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: "ListPlayerSnapshotsParams",
Field: "offset",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListPlayerSnapshotsParams()
require.ErrorIs(t, params.SetOffset(tt.args.offset), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.offset, params.Offset())
})
}
}

View File

@ -274,20 +274,21 @@ func (params CreateServerParams) VersionCode() string {
}
type UpdateServerParams struct {
config NullServerConfig
buildingInfo NullBuildingInfo
unitInfo NullUnitInfo
numTribes NullInt
tribeDataSyncedAt NullTime
numPlayers NullInt
playerDataSyncedAt NullTime
numVillages NullInt
numPlayerVillages NullInt
numBarbarianVillages NullInt
numBonusVillages NullInt
villageDataSyncedAt NullTime
ennoblementDataSyncedAt NullTime
tribeSnapshotsCreatedAt NullTime
config NullServerConfig
buildingInfo NullBuildingInfo
unitInfo NullUnitInfo
numTribes NullInt
tribeDataSyncedAt NullTime
numPlayers NullInt
playerDataSyncedAt NullTime
numVillages NullInt
numPlayerVillages NullInt
numBarbarianVillages NullInt
numBonusVillages NullInt
villageDataSyncedAt NullTime
ennoblementDataSyncedAt NullTime
tribeSnapshotsCreatedAt NullTime
playerSnapshotsCreatedAt NullTime
}
const updateServerParamsModelName = "UpdateServerParams"
@ -429,6 +430,15 @@ func (params *UpdateServerParams) SetTribeSnapshotsCreatedAt(tribeSnapshotsCreat
return nil
}
func (params *UpdateServerParams) PlayerSnapshotsCreatedAt() NullTime {
return params.playerSnapshotsCreatedAt
}
func (params *UpdateServerParams) SetPlayerSnapshotsCreatedAt(playerSnapshotsCreatedAt NullTime) error {
params.playerSnapshotsCreatedAt = playerSnapshotsCreatedAt
return nil
}
//nolint:gocyclo
func (params *UpdateServerParams) IsZero() bool {
return !params.config.Valid &&
@ -441,7 +451,8 @@ func (params *UpdateServerParams) IsZero() bool {
!params.numVillages.Valid &&
!params.villageDataSyncedAt.Valid &&
!params.ennoblementDataSyncedAt.Valid &&
!params.tribeSnapshotsCreatedAt.Valid
!params.tribeSnapshotsCreatedAt.Valid &&
!params.playerSnapshotsCreatedAt.Valid
}
type ServerSort uint8

View File

@ -49,6 +49,14 @@ func UnmarshalTribeSnapshotFromDatabase(
}
}
if err := validateIntInRange(tribeID, 1, math.MaxInt); err != nil {
return TribeSnapshot{}, ValidationError{
Model: tribeSnapshotModelName,
Field: "tribeID",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return TribeSnapshot{}, ValidationError{
Model: tribeSnapshotModelName,

View File

@ -9,26 +9,32 @@ import (
)
type PlayerWatermillConsumer struct {
svc *app.PlayerService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
eventServerSyncedTopic string
svc *app.PlayerService
snapshotSvc *app.PlayerSnapshotService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
eventServerSyncedTopic string
cmdCreateSnapshotsTopic string
}
func NewPlayerWatermillConsumer(
svc *app.PlayerService,
snapshotSvc *app.PlayerSnapshotService,
subscriber message.Subscriber,
logger watermill.LoggerAdapter,
marshaler watermillmsg.Marshaler,
eventServerSyncedTopic string,
cmdCreateSnapshotsTopic string,
) *PlayerWatermillConsumer {
return &PlayerWatermillConsumer{
svc: svc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
eventServerSyncedTopic: eventServerSyncedTopic,
svc: svc,
snapshotSvc: snapshotSvc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
eventServerSyncedTopic: eventServerSyncedTopic,
cmdCreateSnapshotsTopic: cmdCreateSnapshotsTopic,
}
}
@ -39,6 +45,12 @@ func (c *PlayerWatermillConsumer) Register(router *message.Router) {
c.subscriber,
c.sync,
)
router.AddNoPublisherHandler(
"PlayerConsumer.createSnapshots",
c.cmdCreateSnapshotsTopic,
c.subscriber,
c.createSnapshots,
)
}
func (c *PlayerWatermillConsumer) sync(msg *message.Message) error {
@ -61,3 +73,29 @@ func (c *PlayerWatermillConsumer) sync(msg *message.Message) error {
return c.svc.Sync(msg.Context(), payload)
}
func (c *PlayerWatermillConsumer) createSnapshots(msg *message.Message) error {
var rawPayload watermillmsg.CreateSnapshotsCmdPayload
if err := c.marshaler.Unmarshal(msg, &rawPayload); err != nil {
c.logger.Error("couldn't unmarshal payload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
payload, err := domain.NewCreateSnapshotsCmdPayload(
rawPayload.ServerKey,
rawPayload.VersionCode,
rawPayload.VersionTimezone,
rawPayload.Date,
)
if err != nil {
c.logger.Error("couldn't construct domain.CreateSnapshotsCmdPayload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
return c.snapshotSvc.Create(msg.Context(), payload)
}

View File

@ -9,17 +9,18 @@ import (
)
type ServerWatermillConsumer struct {
svc *app.ServerService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
cmdSyncTopic string
eventServerSyncedTopic string
eventTribesSyncedTopic string
eventPlayersSyncedTopic string
eventVillagesSyncedTopic string
eventEnnoblementsSyncedTopic string
eventTribeSnapshotsCreatedTopic string
svc *app.ServerService
subscriber message.Subscriber
logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler
cmdSyncTopic string
eventServerSyncedTopic string
eventTribesSyncedTopic string
eventPlayersSyncedTopic string
eventVillagesSyncedTopic string
eventEnnoblementsSyncedTopic string
eventTribeSnapshotsCreatedTopic string
eventPlayerSnapshotsCreatedTopic string
}
func NewServerWatermillConsumer(
@ -34,19 +35,21 @@ func NewServerWatermillConsumer(
eventVillagesSyncedTopic string,
eventEnnoblementsSyncedTopic string,
eventTribeSnapshotsCreatedTopic string,
eventPlayerSnapshotsCreatedTopic string,
) *ServerWatermillConsumer {
return &ServerWatermillConsumer{
svc: svc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
cmdSyncTopic: cmdSyncTopic,
eventServerSyncedTopic: eventServerSyncedTopic,
eventTribesSyncedTopic: eventTribesSyncedTopic,
eventPlayersSyncedTopic: eventPlayersSyncedTopic,
eventVillagesSyncedTopic: eventVillagesSyncedTopic,
eventEnnoblementsSyncedTopic: eventEnnoblementsSyncedTopic,
eventTribeSnapshotsCreatedTopic: eventTribeSnapshotsCreatedTopic,
svc: svc,
subscriber: subscriber,
logger: logger,
marshaler: marshaler,
cmdSyncTopic: cmdSyncTopic,
eventServerSyncedTopic: eventServerSyncedTopic,
eventTribesSyncedTopic: eventTribesSyncedTopic,
eventPlayersSyncedTopic: eventPlayersSyncedTopic,
eventVillagesSyncedTopic: eventVillagesSyncedTopic,
eventEnnoblementsSyncedTopic: eventEnnoblementsSyncedTopic,
eventTribeSnapshotsCreatedTopic: eventTribeSnapshotsCreatedTopic,
eventPlayerSnapshotsCreatedTopic: eventPlayerSnapshotsCreatedTopic,
}
}
@ -88,6 +91,12 @@ func (c *ServerWatermillConsumer) Register(router *message.Router) {
c.subscriber,
c.updateTribeSnapshotsCreatedAt,
)
router.AddNoPublisherHandler(
"ServerConsumer.updatePlayerSnapshotsCreatedAt",
c.eventPlayerSnapshotsCreatedTopic,
c.subscriber,
c.updatePlayerSnapshotsCreatedAt,
)
}
func (c *ServerWatermillConsumer) sync(msg *message.Message) error {
@ -261,3 +270,27 @@ func (c *ServerWatermillConsumer) updateTribeSnapshotsCreatedAt(msg *message.Mes
return c.svc.UpdateTribeSnapshotsCreatedAt(msg.Context(), payload)
}
func (c *ServerWatermillConsumer) updatePlayerSnapshotsCreatedAt(msg *message.Message) error {
var rawPayload watermillmsg.SnapshotsCreatedEventPayload
if err := c.marshaler.Unmarshal(msg, &rawPayload); err != nil {
c.logger.Error("couldn't unmarshal payload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
payload, err := domain.NewSnapshotsCreatedEventPayload(
rawPayload.ServerKey,
rawPayload.VersionCode,
)
if err != nil {
c.logger.Error("couldn't construct domain.SnapshotsCreatedEventPayload", err, watermill.LogFields{
"handler": message.HandlerNameFromCtx(msg.Context()),
})
return nil
}
return c.svc.UpdatePlayerSnapshotsCreatedAt(msg.Context(), payload)
}

View File

@ -167,6 +167,7 @@ func TestDataSync(t *testing.T) {
villageEventSynced,
"",
"",
"",
),
port.NewTribeWatermillConsumer(
tribeSvc,
@ -178,7 +179,15 @@ func TestDataSync(t *testing.T) {
villageEventSynced,
"",
),
port.NewPlayerWatermillConsumer(playerSvc, playerSub, nopLogger, marshaler, serverEventSynced),
port.NewPlayerWatermillConsumer(
playerSvc,
nil,
playerSub,
nopLogger,
marshaler,
serverEventSynced,
"",
),
port.NewVillageWatermillConsumer(villageSvc, villageSub, nopLogger, marshaler, serverEventSynced),
)

View File

@ -148,6 +148,7 @@ func TestEnnoblementSync(t *testing.T) {
"",
ennoblementEventSynced,
"",
"",
),
port.NewEnnoblementWatermillConsumer(ennoblementSvc, ennoblementSub, nopLogger, marshaler, ennoblementCmdSync),
)