Compare commits

...

18 Commits

Author SHA1 Message Date
Dawid Wysokiński 9d5987f74c feat: new endpoint GET /api/v2/versions/{versionCode}/servers/{serverKey}/snapshots (#52)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
Reviewed-on: #52
2024-05-13 05:12:05 +00:00
Dawid Wysokiński 4ca2ea9361
chore: update prod Dockerfile
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-12 09:47:46 +02:00
Dawid Wysokiński 6e2c54679e
feat: postgres - set application name
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
2024-05-11 17:30:06 +02:00
Dawid Wysokiński 7de443c652
fix: server_snapshots - incorrect num_inactive_tribes
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-11 17:23:40 +02:00
Dawid Wysokiński c6d7ad8965
chore: update prod/Dockerfile
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-10 07:04:36 +02:00
Dawid Wysokiński 98361f3ae0 fix: ci/cd - test step timeouts (#51)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #51
2024-05-10 04:57:35 +00:00
Dawid Wysokiński 08a7f0c504 feat: server snapshots (#49)
ci/woodpecker/push/govulncheck Pipeline failed Details
ci/woodpecker/push/test Pipeline failed Details
Reviewed-on: #49
2024-05-09 07:49:04 +00:00
Renovate e3fa23d0c4 chore(deps): update module github.com/brianvoe/gofakeit/v7 to v7.0.3 (#48)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #48
Co-authored-by: Renovate <renovate@dwysokinski.me>
Co-committed-by: Renovate <renovate@dwysokinski.me>
2024-05-08 04:31:00 +00:00
Dawid Wysokiński f8c9bdb321 feat: add more stats to the Server model (#47)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #47
2024-05-07 06:15:37 +00:00
Dawid Wysokiński 4d6c1c8982 refactor: server - rename num_tribes and num_players (#46)
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/tag/release Pipeline was successful Details
ci/woodpecker/tag/deployment Pipeline was successful Details
ci/woodpecker/push/govulncheck Pipeline was successful Details
Reviewed-on: #46
2024-05-06 04:50:24 +00:00
Dawid Wysokiński 1c1758ce7d refactor: server - rename *_updated_at columns (#45)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
Reviewed-on: #45
2024-05-04 05:26:22 +00:00
Dawid Wysokiński d05d7aaacb
chore: bump golangci-lint to v1.58.0
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-05-04 06:57:03 +02:00
Dawid Wysokiński 68b715306d
refactor: versionModelName should be const, not var
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
2024-04-09 07:51:20 +02:00
Dawid Wysokiński 21e530aa0f
refactor: cmd_serve - rename flag variables
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-04-08 07:12:08 +02:00
Dawid Wysokiński be824acaec
refactor: runConsumer - minor changes to the initialization/cleanup sequence
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-04-08 07:08:11 +02:00
Dawid Wysokiński 3fe539db19
refactor: move logger.Debug call to waitForShutdownSignal
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
2024-04-08 06:41:51 +02:00
Renovate 24b8ffd32c chore(deps): update module github.com/getkin/kin-openapi to v0.124.0 (#42)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
Reviewed-on: #42
Co-authored-by: Renovate <renovate@dwysokinski.me>
Co-committed-by: Renovate <renovate@dwysokinski.me>
2024-04-07 04:09:36 +00:00
Dawid Wysokiński 03e0e4149b
fix: incorrect appName
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details
ci/woodpecker/cron/govulncheck Pipeline was successful Details
2024-04-06 07:22:49 +02:00
88 changed files with 5183 additions and 329 deletions

View File

@ -57,10 +57,12 @@ linters:
- inamedparam - inamedparam
- sloglint - sloglint
- revive - revive
- gomnd - mnd
- forbidigo - forbidigo
- copyloopvar - copyloopvar
- intrange - intrange
- fatcontext
- canonicalheader
linters-settings: linters-settings:
gocyclo: gocyclo:
@ -123,7 +125,7 @@ linters-settings:
enable-all: true enable-all: true
sloglint: sloglint:
attr-only: true attr-only: true
gomnd: mnd:
ignored-functions: ignored-functions:
- strconv.FormatInt - strconv.FormatInt
- strconv.ParseInt - strconv.ParseInt

View File

@ -6,7 +6,7 @@ repos:
stages: [commit-msg] stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"] additional_dependencies: ["@commitlint/config-conventional"]
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v1.57.2 rev: v1.58.1
hooks: hooks:
- id: golangci-lint - id: golangci-lint
- repo: https://github.com/hadolint/hadolint - repo: https://github.com/hadolint/hadolint

View File

@ -39,10 +39,10 @@ steps:
TESTS_RABBITMQ_CONNECTION_STRING: TESTS_RABBITMQ_CONNECTION_STRING:
amqp://twhelp:twhelp@rmq:5672/ amqp://twhelp:twhelp@rmq:5672/
commands: commands:
- go test -race -coverprofile=coverage.txt -covermode=atomic ./... - go test -parallel 2 -race -coverprofile=coverage.txt -covermode=atomic ./...
lint: lint:
image: golangci/golangci-lint:v1.57 image: golangci/golangci-lint:v1.58
pull: true pull: true
depends_on: depends_on:
- generate - generate

View File

@ -16,7 +16,7 @@ install-git-hooks:
.PHONY: install-golangci-lint .PHONY: install-golangci-lint
install-golangci-lint: install-golangci-lint:
@echo "Installing github.com/golangci/golangci-lint..." @echo "Installing github.com/golangci/golangci-lint..."
@(test -f $(GOLANGCI_LINT_PATH) && echo "github.com/golangci/golangci-lint is already installed. Skipping...") || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.57.2 @(test -f $(GOLANGCI_LINT_PATH) && echo "github.com/golangci/golangci-lint is already installed. Skipping...") || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.58.1
.PHONY: install-oapi-codegen .PHONY: install-oapi-codegen
install-oapi-codegen: install-oapi-codegen:

View File

@ -471,6 +471,25 @@ paths:
$ref: "#/components/responses/ListTribeChangesResponse" $ref: "#/components/responses/ListTribeChangesResponse"
default: default:
$ref: "#/components/responses/ErrorResponse" $ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers/{serverKey}/snapshots:
get:
operationId: listServerServerSnapshots
tags:
- versions
- servers
- snapshots
description: List the given server's snapshots
parameters:
- $ref: "#/components/parameters/VersionCodePathParam"
- $ref: "#/components/parameters/ServerKeyPathParam"
- $ref: "#/components/parameters/CursorQueryParam"
- $ref: "#/components/parameters/LimitQueryParam"
- $ref: "#/components/parameters/ServerSnapshotSortQueryParam"
responses:
200:
$ref: "#/components/responses/ListServerSnapshotsResponse"
default:
$ref: "#/components/responses/ErrorResponse"
/v2/versions/{versionCode}/servers/{serverKey}/tribes/{tribeId}/snapshots: /v2/versions/{versionCode}/servers/{serverKey}/tribes/{tribeId}/snapshots:
get: get:
operationId: listTribeTribeSnapshots operationId: listTribeTribeSnapshots
@ -584,7 +603,11 @@ components:
- open - open
- url - url
- numPlayers - numPlayers
- numActivePlayers
- numInactivePlayers
- numTribes - numTribes
- numActiveTribes
- numInactiveTribes
- numVillages - numVillages
- numBarbarianVillages - numBarbarianVillages
- numBonusVillages - numBonusVillages
@ -601,11 +624,21 @@ components:
example: https://en138.tribalwars.net example: https://en138.tribalwars.net
numPlayers: numPlayers:
type: integer type: integer
description: numActivePlayers+numInactivePlayers
numActivePlayers:
type: integer
numInactivePlayers:
type: integer
playerDataSyncedAt: playerDataSyncedAt:
type: string type: string
format: date-time format: date-time
numTribes: numTribes:
type: integer type: integer
description: numActiveTribes+numInactiveTribes
numActiveTribes:
type: integer
numInactiveTribes:
type: integer
tribeDataSyncedAt: tribeDataSyncedAt:
type: string type: string
format: date-time format: date-time
@ -1580,6 +1613,52 @@ components:
createdAt: createdAt:
type: string type: string
format: date-time format: date-time
ServerSnapshot:
type: object
required:
- id
- server
- numPlayers
- numActivePlayers
- numInactivePlayers
- numTribes
- numActiveTribes
- numInactiveTribes
- numVillages
- numBarbarianVillages
- numBonusVillages
- numPlayerVillages
- date
properties:
id:
$ref: "#/components/schemas/IntId"
server:
$ref: "#/components/schemas/ServerMeta"
numPlayers:
type: integer
description: numActivePlayers+numInactivePlayers
numActivePlayers:
type: integer
numInactivePlayers:
type: integer
numTribes:
type: integer
description: numActiveTribes+numInactiveTribes
numActiveTribes:
type: integer
numInactiveTribes:
type: integer
numVillages:
type: integer
numBarbarianVillages:
type: integer
numBonusVillages:
type: integer
numPlayerVillages:
type: integer
date:
type: string
format: date
TribeSnapshot: TribeSnapshot:
type: object type: object
required: required:
@ -1661,7 +1740,7 @@ components:
x-go-type-skip-optional-pointer: true x-go-type-skip-optional-pointer: true
allOf: allOf:
- $ref: "#/components/schemas/CursorString" - $ref: "#/components/schemas/CursorString"
PaginationResponse: Pagination:
type: object type: object
properties: properties:
cursor: cursor:
@ -1826,6 +1905,20 @@ components:
- date:ASC - date:ASC
- date:DESC - date:DESC
maxItems: 1 maxItems: 1
ServerSnapshotSortQueryParam:
name: sort
in: query
description: Order matters!
schema:
type: array
default:
- date:ASC
items:
type: string
enum:
- date:ASC
- date:DESC
maxItems: 1
PlayerSnapshotSortQueryParam: PlayerSnapshotSortQueryParam:
name: sort name: sort
in: query in: query
@ -1891,7 +1984,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -1917,7 +2010,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -1976,7 +2069,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -2002,7 +2095,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -2017,7 +2110,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -2043,7 +2136,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -2069,7 +2162,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -2084,7 +2177,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -2093,13 +2186,28 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/TribeChange" $ref: "#/components/schemas/TribeChange"
ListServerSnapshotsResponse:
description: ""
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Pagination"
- type: object
required:
- data
properties:
data:
type: array
items:
$ref: "#/components/schemas/ServerSnapshot"
ListTribeSnapshotsResponse: ListTribeSnapshotsResponse:
description: "" description: ""
content: content:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data
@ -2114,7 +2222,7 @@ components:
application/json: application/json:
schema: schema:
allOf: allOf:
- $ref: "#/components/schemas/PaginationResponse" - $ref: "#/components/schemas/Pagination"
- type: object - type: object
required: required:
- data - data

View File

@ -1,9 +1,9 @@
FROM --platform=$BUILDPLATFORM alpine:3.19 FROM alpine:3.19
LABEL maintainer="contact@twhelp.app" LABEL maintainer="contact@twhelp.app"
ARG TARGETOS="linux" ARG TARGETOS
ARG TARGETARCH="amd64" ARG TARGETARCH
ARG CI_COMMIT_TAG="v3.0.0" ARG CI_COMMIT_TAG
RUN apk --no-cache add ca-certificates tzdata wget && \ RUN apk --no-cache add ca-certificates tzdata wget && \
wget --progress=dot:giga -O /usr/bin/twhelp https://gitea.dwysokinski.me/twhelp/core/releases/download/${CI_COMMIT_TAG}/core_${CI_COMMIT_TAG##v}_${TARGETOS}_${TARGETARCH} && \ wget --progress=dot:giga -O /usr/bin/twhelp https://gitea.dwysokinski.me/twhelp/core/releases/download/${CI_COMMIT_TAG}/core_${CI_COMMIT_TAG##v}_${TARGETOS}_${TARGETARCH} && \

View File

@ -23,30 +23,30 @@ var (
} }
dbFlagMaxIdleConns = &cli.IntFlag{ dbFlagMaxIdleConns = &cli.IntFlag{
Name: "db.maxIdleConns", Name: "db.maxIdleConns",
Value: 2, //nolint:gomnd Value: 2, //nolint:mnd
EnvVars: []string{"DB_MAX_IDLE_CONNS"}, EnvVars: []string{"DB_MAX_IDLE_CONNS"},
Usage: "https://pkg.go.dev/database/sql#DB.SetMaxIdleConns", Usage: "https://pkg.go.dev/database/sql#DB.SetMaxIdleConns",
} }
dbFlagMaxOpenConns = &cli.IntFlag{ dbFlagMaxOpenConns = &cli.IntFlag{
Name: "db.maxOpenConns", Name: "db.maxOpenConns",
Value: runtime.NumCPU() * 4, //nolint:gomnd Value: runtime.NumCPU() * 4, //nolint:mnd
EnvVars: []string{"DB_MAX_OPEN_CONNS"}, EnvVars: []string{"DB_MAX_OPEN_CONNS"},
Usage: "https://pkg.go.dev/database/sql#DB.SetMaxOpenConns", Usage: "https://pkg.go.dev/database/sql#DB.SetMaxOpenConns",
} }
dbFlagConnMaxLifetime = &cli.DurationFlag{ dbFlagConnMaxLifetime = &cli.DurationFlag{
Name: "db.connMaxLifetime", Name: "db.connMaxLifetime",
Value: 30 * time.Minute, //nolint:gomnd Value: 30 * time.Minute, //nolint:mnd
EnvVars: []string{"DB_CONN_MAX_LIFETIME"}, EnvVars: []string{"DB_CONN_MAX_LIFETIME"},
Usage: "https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime", Usage: "https://pkg.go.dev/database/sql#DB.SetConnMaxLifetime",
} }
dbFlagReadTimeout = &cli.DurationFlag{ dbFlagReadTimeout = &cli.DurationFlag{
Name: "db.readTimeout", Name: "db.readTimeout",
Value: 10 * time.Second, //nolint:gomnd Value: 10 * time.Second, //nolint:mnd
EnvVars: []string{"DB_READ_TIMEOUT"}, EnvVars: []string{"DB_READ_TIMEOUT"},
} }
dbFlagWriteTimeout = &cli.DurationFlag{ dbFlagWriteTimeout = &cli.DurationFlag{
Name: "db.writeTimeout", Name: "db.writeTimeout",
Value: 5 * time.Second, //nolint:gomnd Value: 5 * time.Second, //nolint:mnd
EnvVars: []string{"DB_WRITE_TIMEOUT"}, EnvVars: []string{"DB_WRITE_TIMEOUT"},
} }
dbFlags = []cli.Flag{ dbFlags = []cli.Flag{
@ -59,8 +59,9 @@ var (
} }
) )
func newBunDBFromFlags(c *cli.Context) (*bun.DB, error) { func newBunDBFromFlags(c *cli.Context, applicationName string) (*bun.DB, error) {
return newBunDB(bundDBConfig{ return newBunDB(bundDBConfig{
applicationName: applicationName,
connectionString: c.String(dbFlagConnectionString.Name), connectionString: c.String(dbFlagConnectionString.Name),
maxOpenConns: c.Int(dbFlagMaxOpenConns.Name), maxOpenConns: c.Int(dbFlagMaxOpenConns.Name),
maxIdleConns: c.Int(dbFlagMaxIdleConns.Name), maxIdleConns: c.Int(dbFlagMaxIdleConns.Name),
@ -72,6 +73,7 @@ func newBunDBFromFlags(c *cli.Context) (*bun.DB, error) {
type bundDBConfig struct { type bundDBConfig struct {
connectionString string connectionString string
applicationName string
maxOpenConns int maxOpenConns int
maxIdleConns int maxIdleConns int
connMaxLifetime time.Duration connMaxLifetime time.Duration
@ -99,6 +101,7 @@ func newSQLDB(cfg bundDBConfig) *sql.DB {
pgdriver.WithDSN(cfg.connectionString), pgdriver.WithDSN(cfg.connectionString),
pgdriver.WithReadTimeout(cfg.readTimeout), pgdriver.WithReadTimeout(cfg.readTimeout),
pgdriver.WithWriteTimeout(cfg.writeTimeout), pgdriver.WithWriteTimeout(cfg.writeTimeout),
pgdriver.WithApplicationName(cfg.applicationName),
)) ))
db.SetMaxOpenConns(cfg.maxOpenConns) db.SetMaxOpenConns(cfg.maxOpenConns)
db.SetMaxIdleConns(cfg.maxIdleConns) db.SetMaxIdleConns(cfg.maxIdleConns)

View File

@ -18,6 +18,7 @@ import (
"github.com/ThreeDotsLabs/watermill-amqp/v2/pkg/amqp" "github.com/ThreeDotsLabs/watermill-amqp/v2/pkg/amqp"
"github.com/ThreeDotsLabs/watermill/message" "github.com/ThreeDotsLabs/watermill/message"
"github.com/ThreeDotsLabs/watermill/message/router/middleware" "github.com/ThreeDotsLabs/watermill/message/router/middleware"
"github.com/ettle/strcase"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -49,18 +50,32 @@ var cmdConsumer = &cli.Command{
c.String(rmqFlagTopicSyncServersCmd.Name), c.String(rmqFlagTopicSyncServersCmd.Name),
c.String(rmqFlagTopicServerSyncedEvent.Name), c.String(rmqFlagTopicServerSyncedEvent.Name),
) )
serverSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
marshaler,
c.String(rmqFlagTopicCreateServerSnapshotCmd.Name),
"",
)
twSvc, err := newTWServiceFromFlags(c) twSvc, err := newTWServiceFromFlags(c)
if err != nil { if err != nil {
return err return err
} }
serverSvc := app.NewServerService(adapter.NewServerBunRepository(db), twSvc, serverPublisher)
serverSnapshotSvc := app.NewServerSnapshotService(
adapter.NewServerSnapshotBunRepository(db),
serverSvc,
serverSnapshotPublisher,
)
consumer := port.NewServerWatermillConsumer( consumer := port.NewServerWatermillConsumer(
app.NewServerService(adapter.NewServerBunRepository(db), twSvc, serverPublisher), serverSvc,
serverSnapshotSvc,
subscriber, subscriber,
logger, logger,
marshaler, marshaler,
c.String(rmqFlagTopicSyncServersCmd.Name), c.String(rmqFlagTopicSyncServersCmd.Name),
c.String(rmqFlagTopicCreateServerSnapshotCmd.Name),
c.String(rmqFlagTopicServerSyncedEvent.Name), c.String(rmqFlagTopicServerSyncedEvent.Name),
c.String(rmqFlagTopicTribesSyncedEvent.Name), c.String(rmqFlagTopicTribesSyncedEvent.Name),
c.String(rmqFlagTopicPlayersSyncedEvent.Name), c.String(rmqFlagTopicPlayersSyncedEvent.Name),
@ -299,18 +314,9 @@ type registerConsumerHandlersFunc func(
//nolint:gocyclo //nolint:gocyclo
func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerHandlersFunc) error { func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerHandlersFunc) error {
ctx, cancel := context.WithCancel(c.Context) logger := loggerFromCtx(c.Context)
defer cancel()
logger := loggerFromCtx(ctx)
watermillLogger := newWatermillLogger(logger) watermillLogger := newWatermillLogger(logger)
var wg sync.WaitGroup
defer func() {
// it's required for the graceful shutdown
wg.Wait()
}()
amqpConn, err := newAMQPConnectionFromFlags(c, watermillLogger) amqpConn, err := newAMQPConnectionFromFlags(c, watermillLogger)
if err != nil { if err != nil {
return err return err
@ -341,19 +347,12 @@ func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerH
} }
}() }()
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, strcase.ToSnake(name))
if err != nil { if err != nil {
return err return err
} }
defer closeBunDB(bunDB, logger) defer closeBunDB(bunDB, logger)
healthObserver := healthfile.LiveObserver(health.New(), "/tmp/live")
defer func() {
if closeErr := healthObserver.Close(); closeErr != nil {
logger.Warn("couldn't close health observer", slog.Any("error", closeErr))
}
}()
router, err := newWatermillRouter(watermillLogger) router, err := newWatermillRouter(watermillLogger)
if err != nil { if err != nil {
return err return err
@ -371,6 +370,22 @@ func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerH
return err return err
} }
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
var wg sync.WaitGroup
defer func() {
// it's required for the graceful shutdown
wg.Wait()
}()
healthObserver := healthfile.LiveObserver(health.New(), "/tmp/live")
defer func() {
if closeErr := healthObserver.Close(); closeErr != nil {
logger.Warn("couldn't close health observer", slog.Any("error", closeErr))
}
}()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
@ -391,7 +406,7 @@ func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerH
logger.Info("consumer is up and running", slog.String("name", name)) logger.Info("consumer is up and running", slog.String("name", name))
} }
waitForShutdownSignal(ctx) waitForShutdownSignal(ctx, logger)
if closeErr := router.Close(); closeErr != nil { if closeErr := router.Close(); closeErr != nil {
logger.Warn("couldn't close router", slog.Any("error", err)) logger.Warn("couldn't close router", slog.Any("error", err))

View File

@ -45,7 +45,7 @@ var cmdDB = &cli.Command{
logger := loggerFromCtx(c.Context) logger := loggerFromCtx(c.Context)
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, "migrations")
if err != nil { if err != nil {
return err return err
} }
@ -94,7 +94,7 @@ var cmdDB = &cli.Command{
logger := loggerFromCtx(c.Context) logger := loggerFromCtx(c.Context)
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, "migrations")
if err != nil { if err != nil {
return err return err
} }

View File

@ -47,7 +47,7 @@ var (
} }
}() }()
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, "data_sync_job")
if err != nil { if err != nil {
return err return err
} }
@ -103,7 +103,7 @@ var (
} }
}() }()
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, "ennoblement_sync_job")
if err != nil { if err != nil {
return err return err
} }
@ -165,19 +165,24 @@ var (
} }
}() }()
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, "snapshot_creation_job")
if err != nil { if err != nil {
return err return err
} }
defer closeBunDB(bunDB, logger) defer closeBunDB(bunDB, logger)
serverSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher,
newWatermillMarshaler(),
c.String(rmqFlagTopicCreateServerSnapshotCmd.Name),
"",
)
tribeSnapshotPublisher := adapter.NewSnapshotWatermillPublisher( tribeSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher, publisher,
newWatermillMarshaler(), newWatermillMarshaler(),
c.String(rmqFlagTopicCreateTribeSnapshotsCmd.Name), c.String(rmqFlagTopicCreateTribeSnapshotsCmd.Name),
c.String(rmqFlagTopicTribeSnapshotsCreatedEvent.Name), c.String(rmqFlagTopicTribeSnapshotsCreatedEvent.Name),
) )
playerSnapshotPublisher := adapter.NewSnapshotWatermillPublisher( playerSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
publisher, publisher,
newWatermillMarshaler(), newWatermillMarshaler(),
@ -187,7 +192,13 @@ var (
versionSvc := app.NewVersionService(adapter.NewVersionBunRepository(bunDB)) versionSvc := app.NewVersionService(adapter.NewVersionBunRepository(bunDB))
serverSvc := app.NewServerService(adapter.NewServerBunRepository(bunDB), nil, nil) serverSvc := app.NewServerService(adapter.NewServerBunRepository(bunDB), nil, nil)
snapshotSvc := app.NewSnapshotService(versionSvc, serverSvc, tribeSnapshotPublisher, playerSnapshotPublisher) snapshotSvc := app.NewSnapshotService(
versionSvc,
serverSvc,
serverSnapshotPublisher,
tribeSnapshotPublisher,
playerSnapshotPublisher,
)
shutdownSignalCtx, stop := newShutdownSignalContext(c.Context) shutdownSignalCtx, stop := newShutdownSignalContext(c.Context)
defer stop() defer stop()
@ -231,7 +242,7 @@ var (
} }
}() }()
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, "cleanup_job")
if err != nil { if err != nil {
return err return err
} }

View File

@ -23,66 +23,66 @@ import (
) )
var ( var (
apiServerPortFlag = &cli.UintFlag{ apiServerFlagPort = &cli.UintFlag{
Name: "api.port", Name: "api.port",
EnvVars: []string{"API_PORT"}, EnvVars: []string{"API_PORT"},
Value: 9234, //nolint:gomnd Value: 9234, //nolint:mnd
} }
apiServerHandlerTimeoutFlag = &cli.DurationFlag{ apiServerFlagHandlerTimeout = &cli.DurationFlag{
Name: "api.handlerTimeout", Name: "api.handlerTimeout",
EnvVars: []string{"API_HANDLER_TIMEOUT"}, EnvVars: []string{"API_HANDLER_TIMEOUT"},
Value: 5 * time.Second, //nolint:gomnd Value: 5 * time.Second, //nolint:mnd
Usage: "https://pkg.go.dev/net/http#TimeoutHandler", Usage: "https://pkg.go.dev/net/http#TimeoutHandler",
} }
apiServerReadTimeoutFlag = &cli.DurationFlag{ apiServerFlagReadTimeout = &cli.DurationFlag{
Name: "api.readTimeout", Name: "api.readTimeout",
EnvVars: []string{"API_READ_TIMEOUT"}, EnvVars: []string{"API_READ_TIMEOUT"},
Value: 5 * time.Second, //nolint:gomnd Value: 5 * time.Second, //nolint:mnd
} }
apiServerReadHeaderTimeoutFlag = &cli.DurationFlag{ apiServerFlagReadHeaderTimeout = &cli.DurationFlag{
Name: "api.readHeaderTimeout", Name: "api.readHeaderTimeout",
EnvVars: []string{"API_READ_HEADER_TIMEOUT"}, EnvVars: []string{"API_READ_HEADER_TIMEOUT"},
Value: time.Second, Value: time.Second,
} }
apiServerWriteTimeoutFlag = &cli.DurationFlag{ apiServerFlagWriteTimeout = &cli.DurationFlag{
Name: "api.writeTimeout", Name: "api.writeTimeout",
EnvVars: []string{"API_WRITE_TIMEOUT"}, EnvVars: []string{"API_WRITE_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd Value: 10 * time.Second, //nolint:mnd
} }
apiServerIdleTimeoutFlag = &cli.DurationFlag{ apiServerFlagIdleTimeout = &cli.DurationFlag{
Name: "api.idleTimeout", Name: "api.idleTimeout",
EnvVars: []string{"API_IDLE_TIMEOUT"}, EnvVars: []string{"API_IDLE_TIMEOUT"},
Value: 180 * time.Second, //nolint:gomnd Value: 180 * time.Second, //nolint:mnd
} }
apiServerShutdownTimeoutFlag = &cli.DurationFlag{ apiServerFlagShutdownTimeout = &cli.DurationFlag{
Name: "api.shutdownTimeout", Name: "api.shutdownTimeout",
EnvVars: []string{"API_SHUTDOWN_TIMEOUT"}, EnvVars: []string{"API_SHUTDOWN_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd Value: 10 * time.Second, //nolint:mnd
} }
apiServerOpenAPIEnabledFlag = &cli.BoolFlag{ apiServerFlagOpenAPIEnabled = &cli.BoolFlag{
Name: "api.openApi.enabled", Name: "api.openApi.enabled",
EnvVars: []string{"API_OPENAPI_ENABLED"}, EnvVars: []string{"API_OPENAPI_ENABLED"},
Value: true, Value: true,
} }
apiServerOpenAPISwaggerEnabledFlag = &cli.BoolFlag{ apiServerFlagOpenAPISwaggerEnabled = &cli.BoolFlag{
Name: "api.openApi.swaggerEnabled", Name: "api.openApi.swaggerEnabled",
EnvVars: []string{"API_OPENAPI_SWAGGER_ENABLED"}, EnvVars: []string{"API_OPENAPI_SWAGGER_ENABLED"},
Value: true, Value: true,
} }
apiServerOpenAPIServersFlag = &cli.StringSliceFlag{ apiServerFlagOpenAPIServers = &cli.StringSliceFlag{
Name: "api.openApi.servers", Name: "api.openApi.servers",
EnvVars: []string{"API_OPENAPI_SERVERS"}, EnvVars: []string{"API_OPENAPI_SERVERS"},
} }
apiServerFlags = []cli.Flag{ apiServerFlags = []cli.Flag{
apiServerPortFlag, apiServerFlagPort,
apiServerHandlerTimeoutFlag, apiServerFlagHandlerTimeout,
apiServerReadTimeoutFlag, apiServerFlagReadTimeout,
apiServerReadHeaderTimeoutFlag, apiServerFlagReadHeaderTimeout,
apiServerWriteTimeoutFlag, apiServerFlagWriteTimeout,
apiServerIdleTimeoutFlag, apiServerFlagIdleTimeout,
apiServerOpenAPIEnabledFlag, apiServerFlagOpenAPIEnabled,
apiServerOpenAPISwaggerEnabledFlag, apiServerFlagOpenAPISwaggerEnabled,
apiServerOpenAPIServersFlag, apiServerFlagOpenAPIServers,
} }
) )
@ -99,7 +99,7 @@ var cmdServe = &cli.Command{
logger := loggerFromCtx(c.Context) logger := loggerFromCtx(c.Context)
// deps // deps
bunDB, err := newBunDBFromFlags(c) bunDB, err := newBunDBFromFlags(c, "api")
if err != nil { if err != nil {
return err return err
} }
@ -113,6 +113,7 @@ var cmdServe = &cli.Command{
villageRepo := adapter.NewVillageBunRepository(bunDB) villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB) ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB) tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
serverSnapshotRepo := adapter.NewServerSnapshotBunRepository(bunDB)
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB) tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB)
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB) playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB)
@ -124,6 +125,7 @@ var cmdServe = &cli.Command{
playerSvc := app.NewPlayerService(playerRepo, tribeChangeSvc, nil, nil) playerSvc := app.NewPlayerService(playerRepo, tribeChangeSvc, nil, nil)
villageSvc := app.NewVillageService(villageRepo, nil, nil) villageSvc := app.NewVillageService(villageRepo, nil, nil)
ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil) ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil)
serverSnapshotSvc := app.NewServerSnapshotService(serverSnapshotRepo, serverSvc, nil)
tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, nil) tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, nil)
playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, nil) playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, nil)
@ -132,11 +134,11 @@ var cmdServe = &cli.Command{
server, err := newHTTPServer( server, err := newHTTPServer(
httpServerConfig{ httpServerConfig{
port: c.Uint(apiServerPortFlag.Name), port: c.Uint(apiServerFlagPort.Name),
readTimeout: c.Duration(apiServerReadTimeoutFlag.Name), readTimeout: c.Duration(apiServerFlagReadTimeout.Name),
readHeaderTimeout: c.Duration(apiServerReadHeaderTimeoutFlag.Name), readHeaderTimeout: c.Duration(apiServerFlagReadHeaderTimeout.Name),
writeTimeout: c.Duration(apiServerWriteTimeoutFlag.Name), writeTimeout: c.Duration(apiServerFlagWriteTimeout.Name),
idleTimeout: c.Duration(apiServerIdleTimeoutFlag.Name), idleTimeout: c.Duration(apiServerFlagIdleTimeout.Name),
}, },
func(r chi.Router) error { func(r chi.Router) error {
oapiCfg, oapiCfgErr := newOpenAPIConfigFromFlags(c) oapiCfg, oapiCfgErr := newOpenAPIConfigFromFlags(c)
@ -160,6 +162,7 @@ var cmdServe = &cli.Command{
villageSvc, villageSvc,
ennoblementSvc, ennoblementSvc,
tribeChangeSvc, tribeChangeSvc,
serverSnapshotSvc,
tribeSnapshotSvc, tribeSnapshotSvc,
playerSnapshotSvc, playerSnapshotSvc,
port.WithOpenAPIConfig(oapiCfg), port.WithOpenAPIConfig(oapiCfg),
@ -175,11 +178,9 @@ var cmdServe = &cli.Command{
idleConnsClosed := make(chan struct{}) idleConnsClosed := make(chan struct{})
go func() { go func() {
waitForShutdownSignal(c.Context) waitForShutdownSignal(c.Context, logger)
logger.Debug("received shutdown signal") ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerFlagShutdownTimeout.Name))
ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerShutdownTimeoutFlag.Name))
defer cancel() defer cancel()
if err := server.Shutdown(ctx); err != nil { if err := server.Shutdown(ctx); err != nil {
@ -202,7 +203,7 @@ var cmdServe = &cli.Command{
} }
func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) { func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) {
rawURLs := c.StringSlice(apiServerOpenAPIServersFlag.Name) rawURLs := c.StringSlice(apiServerFlagOpenAPIServers.Name)
servers := make([]port.OpenAPIConfigServer, 0, len(rawURLs)) servers := make([]port.OpenAPIConfigServer, 0, len(rawURLs))
@ -218,8 +219,8 @@ func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) {
} }
return port.OpenAPIConfig{ return port.OpenAPIConfig{
Enabled: c.Bool(apiServerOpenAPIEnabledFlag.Name), Enabled: c.Bool(apiServerFlagOpenAPIEnabled.Name),
SwaggerEnabled: c.Bool(apiServerOpenAPISwaggerEnabledFlag.Name), SwaggerEnabled: c.Bool(apiServerFlagOpenAPISwaggerEnabled.Name),
BasePath: apiBasePath, BasePath: apiBasePath,
Servers: servers, Servers: servers,
}, nil }, nil
@ -239,7 +240,7 @@ func newAPIMiddlewares(c *cli.Context, logger *slog.Logger) chi.Middlewares {
})), })),
middleware.Recoverer, middleware.Recoverer,
func(next http.Handler) http.Handler { func(next http.Handler) http.Handler {
return http.TimeoutHandler(next, c.Duration(apiServerHandlerTimeoutFlag.Name), "Timeout") return http.TimeoutHandler(next, c.Duration(apiServerFlagHandlerTimeout.Name), "Timeout")
}, },
} }
} }

View File

@ -5,7 +5,7 @@ import (
"os" "os"
) )
const appName = "wendia" const appName = "twhelp"
// this flag will be set by the build flags // this flag will be set by the build flags
var version = "development" var version = "development"

View File

@ -31,6 +31,11 @@ var (
Value: "tribes.event.synced", Value: "tribes.event.synced",
EnvVars: []string{"RABBITMQ_TOPIC_TRIBES_SYNCED_EVENT"}, EnvVars: []string{"RABBITMQ_TOPIC_TRIBES_SYNCED_EVENT"},
} }
rmqFlagTopicCreateServerSnapshotCmd = &cli.StringFlag{
Name: "rabbitmq.topic.createServerSnapshotCmd",
Value: "servers.cmd.create_snapshot",
EnvVars: []string{"RABBITMQ_TOPIC_CREATE_SERVER_SNAPSHOT_CMD"},
}
rmqFlagTopicCreateTribeSnapshotsCmd = &cli.StringFlag{ rmqFlagTopicCreateTribeSnapshotsCmd = &cli.StringFlag{
Name: "rabbitmq.topic.createTribeSnapshotsCmd", Name: "rabbitmq.topic.createTribeSnapshotsCmd",
Value: "tribes.cmd.create_snapshots", Value: "tribes.cmd.create_snapshots",
@ -79,6 +84,7 @@ var (
rmqFlags = []cli.Flag{ rmqFlags = []cli.Flag{
rmqFlagConnectionString, rmqFlagConnectionString,
rmqFlagTopicSyncServersCmd, rmqFlagTopicSyncServersCmd,
rmqFlagTopicCreateServerSnapshotCmd,
rmqFlagTopicServerSyncedEvent, rmqFlagTopicServerSyncedEvent,
rmqFlagTopicTribesSyncedEvent, rmqFlagTopicTribesSyncedEvent,
rmqFlagTopicCreateTribeSnapshotsCmd, rmqFlagTopicCreateTribeSnapshotsCmd,

View File

@ -20,7 +20,7 @@ var (
} }
twSvcFlagTimeout = &cli.DurationFlag{ twSvcFlagTimeout = &cli.DurationFlag{
Name: "tw.timeout", Name: "tw.timeout",
Value: 10 * time.Second, //nolint:gomnd Value: 10 * time.Second, //nolint:mnd
EnvVars: []string{"TW_TIMEOUT"}, EnvVars: []string{"TW_TIMEOUT"},
Usage: "https://pkg.go.dev/net/http#Client.Timeout", Usage: "https://pkg.go.dev/net/http#Client.Timeout",
} }

View File

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"log/slog"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -15,8 +16,9 @@ func newShutdownSignalContext(parent context.Context) (context.Context, context.
return signal.NotifyContext(parent, shutdownSignals...) return signal.NotifyContext(parent, shutdownSignals...)
} }
func waitForShutdownSignal(ctx context.Context) { func waitForShutdownSignal(ctx context.Context, logger *slog.Logger) {
ctx, cancel := newShutdownSignalContext(ctx) ctx, cancel := newShutdownSignalContext(ctx)
defer cancel() defer cancel()
<-ctx.Done() <-ctx.Done()
logger.Debug("received shutdown signal")
} }

10
go.mod
View File

@ -6,11 +6,11 @@ require (
gitea.dwysokinski.me/Kichiyaki/chiclientip v0.1.0 gitea.dwysokinski.me/Kichiyaki/chiclientip v0.1.0
github.com/ThreeDotsLabs/watermill v1.3.5 github.com/ThreeDotsLabs/watermill v1.3.5
github.com/ThreeDotsLabs/watermill-amqp/v2 v2.1.1 github.com/ThreeDotsLabs/watermill-amqp/v2 v2.1.1
github.com/brianvoe/gofakeit/v7 v7.0.2 github.com/brianvoe/gofakeit/v7 v7.0.3
github.com/cenkalti/backoff/v4 v4.3.0 github.com/cenkalti/backoff/v4 v4.3.0
github.com/elliotchance/phpserialize v1.4.0 github.com/elliotchance/phpserialize v1.4.0
github.com/ettle/strcase v0.2.0 github.com/ettle/strcase v0.2.0
github.com/getkin/kin-openapi v0.123.0 github.com/getkin/kin-openapi v0.124.0
github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0
@ -26,7 +26,7 @@ require (
github.com/uptrace/bun/driver/pgdriver v1.2.1 github.com/uptrace/bun/driver/pgdriver v1.2.1
github.com/uptrace/bun/driver/sqliteshim v1.2.1 github.com/uptrace/bun/driver/sqliteshim v1.2.1
github.com/uptrace/bun/extra/bundebug v1.2.1 github.com/uptrace/bun/extra/bundebug v1.2.1
github.com/urfave/cli/v2 v2.27.1 github.com/urfave/cli/v2 v2.27.2
go.uber.org/automaxprocs v1.5.3 go.uber.org/automaxprocs v1.5.3
golang.org/x/sync v0.7.0 golang.org/x/sync v0.7.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
@ -40,7 +40,7 @@ require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/containerd/continuity v0.3.0 // indirect github.com/containerd/continuity v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect github.com/docker/docker v20.10.7+incompatible // indirect
@ -86,7 +86,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.16.0 // indirect golang.org/x/mod v0.16.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.18.0 // indirect

20
go.sum
View File

@ -17,8 +17,8 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/brianvoe/gofakeit/v7 v7.0.2 h1:jzYT7Ge3RDHw7J1CM1kwu0OQywV9vbf2qSGxBS72TCY= github.com/brianvoe/gofakeit/v7 v7.0.3 h1:tGCt+eYfhTMWE1ko5G2EO1f/yE44yNpIwUb4h32O0wo=
github.com/brianvoe/gofakeit/v7 v7.0.2/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/brianvoe/gofakeit/v7 v7.0.3/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@ -30,8 +30,8 @@ github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvA
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
@ -55,8 +55,8 @@ github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
@ -206,8 +206,8 @@ github.com/uptrace/bun/driver/sqliteshim v1.2.1/go.mod h1:oJtOPSCDdDHgNw/0jwIGr+
github.com/uptrace/bun/extra/bundebug v1.2.1 h1:85MYpX3QESYI02YerKxUi1CD9mHuLrc2BXs1eOCtQus= github.com/uptrace/bun/extra/bundebug v1.2.1 h1:85MYpX3QESYI02YerKxUi1CD9mHuLrc2BXs1eOCtQus=
github.com/uptrace/bun/extra/bundebug v1.2.1/go.mod h1:sfGKIi0HSGxsTC/sgIHGwpnYduHHYhdMeOIwurgSY+Y= github.com/uptrace/bun/extra/bundebug v1.2.1/go.mod h1:sfGKIi0HSGxsTC/sgIHGwpnYduHHYhdMeOIwurgSY+Y=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
@ -220,8 +220,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=

View File

@ -35,10 +35,11 @@ func (pub *PlayerWatermillPublisher) EventSynced(
for _, p := range payloads { for _, p := range payloads {
msg, err := pub.marshaler.Marshal(ctx, watermillmsg.PlayersSyncedEventPayload{ msg, err := pub.marshaler.Marshal(ctx, watermillmsg.PlayersSyncedEventPayload{
ServerKey: p.ServerKey(), ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(), VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(), ServerURL: p.ServerURL(),
NumPlayers: p.NumPlayers(), NumPlayers: p.NumPlayers(),
NumActivePlayers: p.NumActivePlayers(),
}) })
if err != nil { if err != nil {
return fmt.Errorf("%s: couldn't marshal PlayersSyncedEventPayload: %w", p.ServerKey(), err) return fmt.Errorf("%s: couldn't marshal PlayersSyncedEventPayload: %w", p.ServerKey(), err)

View File

@ -35,10 +35,11 @@ func (pub *TribeWatermillPublisher) EventSynced(
for _, p := range payloads { for _, p := range payloads {
msg, err := pub.marshaler.Marshal(ctx, watermillmsg.TribesSyncedEventPayload{ msg, err := pub.marshaler.Marshal(ctx, watermillmsg.TribesSyncedEventPayload{
ServerKey: p.ServerKey(), ServerKey: p.ServerKey(),
VersionCode: p.VersionCode(), VersionCode: p.VersionCode(),
ServerURL: p.ServerURL(), ServerURL: p.ServerURL(),
NumTribes: p.NumTribes(), NumTribes: p.NumTribes(),
NumActiveTribes: p.NumActiveTribes(),
}) })
if err != nil { if err != nil {
return fmt.Errorf("%s: couldn't marshal TribesSyncedEventPayload: %w", p.ServerKey(), err) return fmt.Errorf("%s: couldn't marshal TribesSyncedEventPayload: %w", p.ServerKey(), err)

View File

@ -111,7 +111,7 @@ func (repo *EnnoblementBunRepository) ListWithRelations(
func (repo *EnnoblementBunRepository) Delete(ctx context.Context, serverKey string, createdAtLTE time.Time) error { func (repo *EnnoblementBunRepository) Delete(ctx context.Context, serverKey string, createdAtLTE time.Time) error {
if _, err := repo.db.NewDelete(). if _, err := repo.db.NewDelete().
Model(&bunmodel.Ennoblement{}). Model((*bunmodel.Ennoblement)(nil)).
Where("server_key = ?", serverKey). Where("server_key = ?", serverKey).
Where("created_at <= ?", createdAtLTE). Where("created_at <= ?", createdAtLTE).
Returning("NULL"). Returning("NULL").

View File

@ -131,6 +131,17 @@ func (repo *PlayerBunRepository) ListWithRelations(
return domain.NewListPlayersWithRelationsResult(separateListResultAndNext(converted, params.Limit())) return domain.NewListPlayersWithRelationsResult(separateListResultAndNext(converted, params.Limit()))
} }
func (repo *PlayerBunRepository) Count(ctx context.Context, params domain.CountPlayersParams) (int, error) {
cnt, err := repo.db.NewSelect().
Model((*bunmodel.Player)(nil)).
Apply(countPlayersParamsApplier{params: params}.apply).
Count(ctx)
if err != nil {
return 0, fmt.Errorf("couldnt count players: %w", err)
}
return cnt, nil
}
func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error { func (repo *PlayerBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
if len(ids) == 0 { if len(ids) == 0 {
return nil return nil
@ -323,3 +334,15 @@ func (a listPlayersParamsApplier) sortToColumnAndDirection(
return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue) return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)
} }
} }
type countPlayersParamsApplier struct {
params domain.CountPlayersParams
}
func (a countPlayersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("player.server_key IN (?)", bun.In(serverKeys))
}
return q
}

View File

@ -109,7 +109,7 @@ func (repo *PlayerSnapshotBunRepository) ListWithRelations(
func (repo *PlayerSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error { func (repo *PlayerSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error {
if _, err := repo.db.NewDelete(). if _, err := repo.db.NewDelete().
Model(&bunmodel.PlayerSnapshot{}). Model((*bunmodel.PlayerSnapshot)(nil)).
Where("server_key = ?", serverKey). Where("server_key = ?", serverKey).
Where("date <= ?", dateLTE). Where("date <= ?", dateLTE).
Returning("NULL"). Returning("NULL").

View File

@ -130,18 +130,32 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
q = q.Set("num_tribes = ?", numTribes.V) q = q.Set("num_tribes = ?", numTribes.V)
} }
if numActiveTribes := a.params.NumActiveTribes(); numActiveTribes.Valid {
q = q.Set("num_active_tribes = ?", numActiveTribes.V)
}
if numInactiveTribes := a.params.NumInactiveTribes(); numInactiveTribes.Valid {
q = q.Set("num_inactive_tribes = ?", numInactiveTribes.V)
}
if tribeDataSyncedAt := a.params.TribeDataSyncedAt(); tribeDataSyncedAt.Valid { if tribeDataSyncedAt := a.params.TribeDataSyncedAt(); tribeDataSyncedAt.Valid {
// TODO: rename this column to tribe_data_synced_at q = q.Set("tribe_data_synced_at = ?", tribeDataSyncedAt.V)
q = q.Set("tribe_data_updated_at = ?", tribeDataSyncedAt.V)
} }
if numPlayers := a.params.NumPlayers(); numPlayers.Valid { if numPlayers := a.params.NumPlayers(); numPlayers.Valid {
q = q.Set("num_players = ?", numPlayers.V) q = q.Set("num_players = ?", numPlayers.V)
} }
if numActivePlayers := a.params.NumActivePlayers(); numActivePlayers.Valid {
q = q.Set("num_active_players = ?", numActivePlayers.V)
}
if numInactivePlayers := a.params.NumInactivePlayers(); numInactivePlayers.Valid {
q = q.Set("num_inactive_players = ?", numInactivePlayers.V)
}
if playerDataSyncedAt := a.params.PlayerDataSyncedAt(); playerDataSyncedAt.Valid { if playerDataSyncedAt := a.params.PlayerDataSyncedAt(); playerDataSyncedAt.Valid {
// TODO: rename this column to player_data_synced_at q = q.Set("player_data_synced_at = ?", playerDataSyncedAt.V)
q = q.Set("player_data_updated_at = ?", playerDataSyncedAt.V)
} }
if numVillages := a.params.NumVillages(); numVillages.Valid { if numVillages := a.params.NumVillages(); numVillages.Valid {
@ -161,13 +175,15 @@ func (a updateServerParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery {
} }
if villageDataSyncedAt := a.params.VillageDataSyncedAt(); villageDataSyncedAt.Valid { if villageDataSyncedAt := a.params.VillageDataSyncedAt(); villageDataSyncedAt.Valid {
// TODO: rename this column to village_data_synced_at q = q.Set("village_data_synced_at = ?", villageDataSyncedAt.V)
q = q.Set("village_data_updated_at = ?", villageDataSyncedAt.V)
} }
if ennoblementDataSyncedAt := a.params.EnnoblementDataSyncedAt(); ennoblementDataSyncedAt.Valid { if ennoblementDataSyncedAt := a.params.EnnoblementDataSyncedAt(); ennoblementDataSyncedAt.Valid {
// TODO: rename this column to ennoblement_data_synced_at q = q.Set("ennoblement_data_synced_at = ?", ennoblementDataSyncedAt.V)
q = q.Set("ennoblement_data_updated_at = ?", ennoblementDataSyncedAt.V) }
if snapshotCreatedAt := a.params.SnapshotCreatedAt(); snapshotCreatedAt.Valid {
q = q.Set("snapshot_created_at = ?", snapshotCreatedAt.V)
} }
if tribeSnapshotsCreatedAt := a.params.TribeSnapshotsCreatedAt(); tribeSnapshotsCreatedAt.Valid { if tribeSnapshotsCreatedAt := a.params.TribeSnapshotsCreatedAt(); tribeSnapshotsCreatedAt.Valid {
@ -216,6 +232,13 @@ func (a listServersParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
) )
} }
if snapshotCreatedAt := a.params.SnapshotCreatedAtLT(); snapshotCreatedAt.Valid {
q = q.Where(
"server.snapshot_created_at < ? OR server.snapshot_created_at is null",
snapshotCreatedAt.V,
)
}
for _, s := range a.params.Sort() { for _, s := range a.params.Sort() {
column, dir, err := a.sortToColumnAndDirection(s) column, dir, err := a.sortToColumnAndDirection(s)
if err != nil { if err != nil {

View File

@ -0,0 +1,206 @@
package adapter
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/bun/bunmodel"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/uptrace/bun"
)
type ServerSnapshotBunRepository struct {
db bun.IDB
}
func NewServerSnapshotBunRepository(db bun.IDB) *ServerSnapshotBunRepository {
return &ServerSnapshotBunRepository{db: db}
}
func (repo *ServerSnapshotBunRepository) Create(
ctx context.Context,
params ...domain.CreateServerSnapshotParams,
) error {
if len(params) == 0 {
return nil
}
now := time.Now()
snapshots := make(bunmodel.ServerSnapshots, 0, len(params))
for _, p := range params {
snapshots = append(snapshots, bunmodel.ServerSnapshot{
ServerKey: p.ServerKey(),
NumPlayers: p.NumPlayers(),
NumActivePlayers: p.NumActivePlayers(),
NumInactivePlayers: p.NumInactivePlayers(),
NumTribes: p.NumTribes(),
NumActiveTribes: p.NumActiveTribes(),
NumInactiveTribes: p.NumInactiveTribes(),
NumVillages: p.NumVillages(),
NumPlayerVillages: p.NumPlayerVillages(),
NumBarbarianVillages: p.NumBarbarianVillages(),
NumBonusVillages: p.NumBonusVillages(),
Date: p.Date(),
CreatedAt: now,
})
}
if _, err := repo.db.NewInsert().
Model(&snapshots).
Ignore().
Returning("").
Exec(ctx); err != nil {
return fmt.Errorf("something went wrong while inserting server snapshots into the db: %w", err)
}
return nil
}
func (repo *ServerSnapshotBunRepository) List(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsResult, error) {
var serverSnapshots bunmodel.ServerSnapshots
if err := repo.db.NewSelect().
Model(&serverSnapshots).
Apply(listServerSnapshotsParamsApplier{params: params}.apply).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.ListServerSnapshotsResult{}, fmt.Errorf("couldn't select server snapshots from the db: %w", err)
}
converted, err := serverSnapshots.ToDomain()
if err != nil {
return domain.ListServerSnapshotsResult{}, err
}
return domain.NewListServerSnapshotsResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *ServerSnapshotBunRepository) ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, error) {
var serverSnapshots bunmodel.ServerSnapshots
if err := repo.db.NewSelect().
Model(&serverSnapshots).
Apply(listServerSnapshotsParamsApplier{params: params}.apply).
Relation("Server", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Column(bunmodel.ServerMetaColumns...)
}).
Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return domain.ListServerSnapshotsWithRelationsResult{}, fmt.Errorf(
"couldn't select server snapshots from the db: %w",
err,
)
}
converted, err := serverSnapshots.ToDomainWithRelations()
if err != nil {
return domain.ListServerSnapshotsWithRelationsResult{}, err
}
return domain.NewListServerSnapshotsWithRelationsResult(separateListResultAndNext(converted, params.Limit()))
}
func (repo *ServerSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error {
if _, err := repo.db.NewDelete().
Model((*bunmodel.ServerSnapshot)(nil)).
Where("server_key = ?", serverKey).
Where("date <= ?", dateLTE).
Returning("NULL").
Exec(ctx); err != nil {
return fmt.Errorf("couldn't delete server snapshots: %w", err)
}
return nil
}
type listServerSnapshotsParamsApplier struct {
params domain.ListServerSnapshotsParams
}
func (a listServerSnapshotsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("ss.server_key IN (?)", bun.In(serverKeys))
}
for _, s := range a.params.Sort() {
column, dir, err := a.sortToColumnAndDirection(s)
if err != nil {
return q.Err(err)
}
q.OrderExpr("? ?", column, dir.Bun())
}
return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor)
}
func (a listServerSnapshotsParamsApplier) 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 err error
var el cursorPaginationApplierDataElement
el.column, el.direction, err = a.sortToColumnAndDirection(s)
if err != nil {
return q.Err(err)
}
switch s {
case domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC:
el.value = cursor.ID()
el.unique = true
case domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortServerKeyDESC:
el.value = cursor.ServerKey()
case domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC:
el.value = cursor.Date()
default:
return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue))
}
cursorApplier.data = append(cursorApplier.data, el)
}
return q.Apply(cursorApplier.apply)
}
func (a listServerSnapshotsParamsApplier) sortToColumnAndDirection(
s domain.ServerSnapshotSort,
) (bun.Safe, sortDirection, error) {
switch s {
case domain.ServerSnapshotSortDateASC:
return "ss.date", sortDirectionASC, nil
case domain.ServerSnapshotSortDateDESC:
return "ss.date", sortDirectionDESC, nil
case domain.ServerSnapshotSortIDASC:
return "ss.id", sortDirectionASC, nil
case domain.ServerSnapshotSortIDDESC:
return "ss.id", sortDirectionDESC, nil
case domain.ServerSnapshotSortServerKeyASC:
return "ss.server_key", sortDirectionASC, nil
case domain.ServerSnapshotSortServerKeyDESC:
return "ss.server_key", sortDirectionDESC, nil
default:
return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)
}
}

View File

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

View File

@ -132,6 +132,17 @@ func (repo *TribeBunRepository) List(
return domain.NewListTribesResult(separateListResultAndNext(converted, params.Limit())) return domain.NewListTribesResult(separateListResultAndNext(converted, params.Limit()))
} }
func (repo *TribeBunRepository) Count(ctx context.Context, params domain.CountTribesParams) (int, error) {
cnt, err := repo.db.NewSelect().
Model((*bunmodel.Tribe)(nil)).
Apply(countTribesParamsApplier{params: params}.apply).
Count(ctx)
if err != nil {
return 0, fmt.Errorf("couldnt count tribes: %w", err)
}
return cnt, nil
}
func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error { func (repo *TribeBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error {
if len(ids) == 0 { if len(ids) == 0 {
return nil return nil
@ -287,3 +298,15 @@ func (a listTribesParamsApplier) sortToColumnAndDirection(
return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue) return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)
} }
} }
type countTribesParamsApplier struct {
params domain.CountTribesParams
}
func (a countTribesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery {
if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 {
q = q.Where("tribe.server_key IN (?)", bun.In(serverKeys))
}
return q
}

View File

@ -105,7 +105,7 @@ func (repo *TribeSnapshotBunRepository) ListWithRelations(
func (repo *TribeSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error { func (repo *TribeSnapshotBunRepository) Delete(ctx context.Context, serverKey string, dateLTE time.Time) error {
if _, err := repo.db.NewDelete(). if _, err := repo.db.NewDelete().
Model(&bunmodel.TribeSnapshot{}). Model((*bunmodel.TribeSnapshot)(nil)).
Where("server_key = ?", serverKey). Where("server_key = ?", serverKey).
Where("date <= ?", dateLTE). Where("date <= ?", dateLTE).
Returning("NULL"). Returning("NULL").

View File

@ -71,12 +71,14 @@ func testPlayerSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repo
key := fmt.Sprintf("%s-%d-%s", p.ServerKey(), p.PlayerID(), p.Date().Format(dateFormat)) key := fmt.Sprintf("%s-%d-%s", p.ServerKey(), p.PlayerID(), p.Date().Format(dateFormat))
for i, ps := range playerSnapshots { for i, ps := range playerSnapshots {
if ps.ServerKey() == p.ServerKey() && ps.PlayerID() == p.PlayerID() && ps.Date().Equal(p.Date()) { //nolint:lll
if ps.ServerKey() == p.ServerKey() && ps.PlayerID() == p.PlayerID() && ps.Date().Format(dateFormat) == p.Date().Format(dateFormat) {
m[key] = append(m[key], i) m[key] = append(m[key], i)
} }
} }
} }
assert.NotEmpty(t, m)
for key, indexes := range m { for key, indexes := range m {
assert.Len(t, indexes, 1, key) assert.Len(t, indexes, 1, key)
} }

View File

@ -664,6 +664,78 @@ func testPlayerRepository(t *testing.T, newRepos func(t *testing.T) repositories
} }
}) })
t.Run("Count", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.CountPlayersParams
assertResult func(t *testing.T, params domain.CountPlayersParams, count int)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.CountPlayersParams {
t.Helper()
return domain.NewCountPlayersParams()
},
assertResult: func(t *testing.T, _ domain.CountPlayersParams, count int) {
t.Helper()
res, err := repos.player.List(ctx, domain.NewListPlayersParams())
require.NoError(t, err)
assert.Len(t, res.Players(), count)
},
},
{
name: "OK: serverKeys",
params: func(t *testing.T) domain.CountPlayersParams {
t.Helper()
res, err := repos.player.List(ctx, domain.NewListPlayersParams())
require.NoError(t, err)
params := domain.NewCountPlayersParams()
require.NoError(t, params.SetServerKeys([]string{res.Players()[0].ServerKey()}))
return params
},
assertResult: func(t *testing.T, params domain.CountPlayersParams, count int) {
t.Helper()
listParams := domain.NewListPlayersParams()
require.NoError(t, listParams.SetServerKeys(params.ServerKeys()))
res, err := repos.player.List(ctx, listParams)
require.NoError(t, err)
assert.Len(t, res.Players(), count)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assertError := tt.assertError
if assertError == nil {
assertError = func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}
}
params := tt.params(t)
cnt, err := repos.player.Count(ctx, params)
assertError(t, err)
tt.assertResult(t, params, cnt)
})
}
})
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -0,0 +1,453 @@
package adapter_test
import (
"cmp"
"context"
"slices"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testServerSnapshotRepository(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.CreateServerSnapshotParams) {
t.Helper()
require.NotEmpty(t, params)
listParams := domain.NewListServerSnapshotsParams()
require.NoError(t, listParams.SetServerKeys([]string{params.ServerKey()}))
res, err := repos.serverSnapshot.List(ctx, listParams)
serverSnapshots := res.ServerSnapshots()
require.NoError(t, err)
date := params.Date().Format(dateFormat)
idx := slices.IndexFunc(serverSnapshots, func(ss domain.ServerSnapshot) bool {
return ss.ServerKey() == params.ServerKey() &&
ss.Date().Format(dateFormat) == date
})
require.GreaterOrEqual(t, idx, 0)
serverSnapshot := serverSnapshots[idx]
assert.Equal(t, params.ServerKey(), serverSnapshot.ServerKey())
assert.Equal(t, params.NumPlayers(), serverSnapshot.NumPlayers())
assert.Equal(t, params.NumActivePlayers(), serverSnapshot.NumActivePlayers())
assert.Equal(t, params.NumInactivePlayers(), serverSnapshot.NumInactivePlayers())
assert.Equal(t, params.NumTribes(), serverSnapshot.NumTribes())
assert.Equal(t, params.NumActiveTribes(), serverSnapshot.NumActiveTribes())
assert.Equal(t, params.NumInactiveTribes(), serverSnapshot.NumInactiveTribes())
assert.Equal(t, params.NumVillages(), serverSnapshot.NumVillages())
assert.Equal(t, params.NumPlayerVillages(), serverSnapshot.NumPlayerVillages())
assert.Equal(t, params.NumBarbarianVillages(), serverSnapshot.NumBarbarianVillages())
assert.Equal(t, params.NumBonusVillages(), serverSnapshot.NumBonusVillages())
assert.Equal(t, date, serverSnapshot.Date().Format(dateFormat))
assert.WithinDuration(t, time.Now(), serverSnapshot.CreatedAt(), time.Minute)
}
assertNoDuplicates := func(t *testing.T, params domain.CreateServerSnapshotParams) {
t.Helper()
listParams := domain.NewListServerSnapshotsParams()
require.NoError(t, listParams.SetServerKeys([]string{params.ServerKey()}))
res, err := repos.serverSnapshot.List(ctx, listParams)
require.NoError(t, err)
serverSnapshots := res.ServerSnapshots()
var indexes []int
for i, ss := range serverSnapshots {
if ss.ServerKey() == params.ServerKey() && ss.Date().Format(dateFormat) == params.Date().Format(dateFormat) {
indexes = append(indexes, i)
}
}
assert.Len(t, indexes, 1)
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
listServersParams := domain.NewListServersParams()
require.NoError(t, listServersParams.SetOpen(domain.NullBool{
V: true,
Valid: true,
}))
require.NoError(t, listServersParams.SetLimit(1))
res, err := repos.server.List(ctx, listServersParams)
require.NoError(t, err)
servers := res.Servers()
require.NotEmpty(t, servers)
date := time.Now()
createParams, err := domain.NewCreateServerSnapshotParams(servers[0], date)
require.NoError(t, err)
require.NoError(t, repos.serverSnapshot.Create(ctx, createParams))
assertCreated(t, createParams)
require.NoError(t, repos.serverSnapshot.Create(ctx, createParams))
assertNoDuplicates(t, createParams)
})
t.Run("OK: len(params) == 0", func(t *testing.T) {
t.Parallel()
require.NoError(t, repos.serverSnapshot.Create(ctx))
})
})
t.Run("List & ListWithRelations", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.ListServerSnapshotsParams
assertResult func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
return domain.NewListServerSnapshotsParams()
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
a.Date().Compare(b.Date()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.False(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
},
},
{
name: "OK: sort=[serverKey DESC, date DESC, id DESC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyDESC,
domain.ServerSnapshotSortDateDESC,
domain.ServerSnapshotSortIDDESC,
}))
return params
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
a.Date().Compare(b.Date()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
},
},
{
name: "OK: sort=[id ASC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
}))
return params
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
},
{
name: "OK: sort=[id DESC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDDESC,
}))
return params
},
assertResult: func(
t *testing.T,
_ domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Compare(a.ID(), b.ID()) * -1
}))
},
},
{
name: "OK: serverKeys",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.ServerSnapshots())
randServerSnapshot := res.ServerSnapshots()[0]
require.NoError(t, params.SetServerKeys([]string{randServerSnapshot.ServerKey()}))
return params
},
assertResult: func(
t *testing.T,
params domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
serverKeys := params.ServerKeys()
serverSnapshots := res.ServerSnapshots()
assert.NotZero(t, serverSnapshots)
for _, ss := range serverSnapshots {
assert.True(t, slices.Contains(serverKeys, ss.ServerKey()))
}
},
},
{
name: "OK: cursor serverKeys sort=[id ASC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.ServerSnapshots()), 2)
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC}))
require.NoError(t, params.SetServerKeys([]string{res.ServerSnapshots()[1].ServerKey()}))
cursor, err := res.ServerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult) {
t.Helper()
serverKeys := params.ServerKeys()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
for _, ss := range serverSnapshots {
assert.GreaterOrEqual(t, ss.ID(), params.Cursor().ID())
assert.True(t, slices.Contains(serverKeys, ss.ServerKey()))
}
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Compare(a.ID(), b.ID())
}))
},
},
{
name: "OK: cursor sort=[serverKey ASC, id ASC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortIDASC,
}))
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.ServerSnapshots()), 2)
cursor, err := res.ServerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
)
}))
assert.GreaterOrEqual(t, serverSnapshots[0].ID(), params.Cursor().ID())
for _, ss := range serverSnapshots {
assert.GreaterOrEqual(t, ss.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: cursor sort=[serverKey DESC, id DESC]",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyDESC,
domain.ServerSnapshotSortIDDESC,
}))
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.Greater(t, len(res.ServerSnapshots()), 2)
cursor, err := res.ServerSnapshots()[1].ToCursor()
require.NoError(t, err)
require.NoError(t, params.SetCursor(cursor))
return params
},
assertResult: func(t *testing.T, params domain.ListServerSnapshotsParams, res domain.ListServerSnapshotsResult) {
t.Helper()
serverSnapshots := res.ServerSnapshots()
assert.NotEmpty(t, serverSnapshots)
assert.True(t, slices.IsSortedFunc(serverSnapshots, func(a, b domain.ServerSnapshot) int {
return cmp.Or(
cmp.Compare(a.ServerKey(), b.ServerKey()),
cmp.Compare(a.ID(), b.ID()),
) * -1
}))
assert.LessOrEqual(t, serverSnapshots[0].ID(), params.Cursor().ID())
for _, ss := range serverSnapshots {
assert.LessOrEqual(t, ss.ServerKey(), params.Cursor().ServerKey())
}
},
},
{
name: "OK: limit=2",
params: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetLimit(2))
return params
},
assertResult: func(
t *testing.T,
params domain.ListServerSnapshotsParams,
res domain.ListServerSnapshotsResult,
) {
t.Helper()
assert.Len(t, res.ServerSnapshots(), params.Limit())
assert.False(t, res.Self().IsZero())
assert.False(t, res.Next().IsZero())
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assertError := tt.assertError
if assertError == nil {
assertError = func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}
}
params := tt.params(t)
res, err := repos.serverSnapshot.List(ctx, params)
assertError(t, err)
tt.assertResult(t, params, res)
resWithRelations, err := repos.serverSnapshot.ListWithRelations(ctx, params)
assertError(t, err)
require.Len(t, resWithRelations.ServerSnapshots(), len(res.ServerSnapshots()))
for i, ss := range resWithRelations.ServerSnapshots() {
assert.Equal(t, res.ServerSnapshots()[i], ss.ServerSnapshot())
assert.Equal(t, ss.ServerSnapshot().ServerKey(), ss.Server().Key())
}
})
}
})
t.Run("Delete", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortIDASC,
}))
res, err := repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
require.NotEmpty(t, res.ServerSnapshots())
randSnapshot := res.ServerSnapshots()[0]
require.NoError(t, repos.serverSnapshot.Delete(ctx, randSnapshot.ServerKey(), randSnapshot.Date()))
require.NoError(t, params.SetServerKeys([]string{randSnapshot.ServerKey()}))
res, err = repos.serverSnapshot.List(ctx, params)
require.NoError(t, err)
assert.NotEmpty(t, res.ServerSnapshots())
for _, ss := range res.ServerSnapshots() {
assert.True(t, ss.Date().After(randSnapshot.Date()))
}
})
})
}

View File

@ -281,6 +281,26 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
} }
}, },
}, },
{
name: "OK: snapshotCreatedAt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
params: func(t *testing.T) domain.ListServersParams {
t.Helper()
params := domain.NewListServersParams()
require.NoError(t, params.SetSnapshotCreatedAtLT(domain.NullTime{
V: snapshotsCreatedAtLT,
Valid: true,
}))
return params
},
assertResult: func(t *testing.T, _ domain.ListServersParams, res domain.ListServersResult) {
t.Helper()
servers := res.Servers()
assert.NotEmpty(t, servers)
for _, s := range servers {
assert.True(t, s.SnapshotCreatedAt().Before(snapshotsCreatedAtLT))
}
},
},
{ {
name: "OK: playerSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339), name: "OK: playerSnapshotsCreatedAtLt=" + snapshotsCreatedAtLT.Format(time.RFC3339),
params: func(t *testing.T) domain.ListServersParams { params: func(t *testing.T) domain.ListServersParams {
@ -498,6 +518,14 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
V: gofakeit.IntRange(0, math.MaxInt), V: gofakeit.IntRange(0, math.MaxInt),
Valid: true, Valid: true,
})) }))
require.NoError(t, updateParams.SetNumActiveTribes(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetNumInactiveTribes(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetTribeDataSyncedAt(domain.NullTime{ require.NoError(t, updateParams.SetTribeDataSyncedAt(domain.NullTime{
V: time.Now(), V: time.Now(),
Valid: true, Valid: true,
@ -506,6 +534,14 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
V: gofakeit.IntRange(0, math.MaxInt), V: gofakeit.IntRange(0, math.MaxInt),
Valid: true, Valid: true,
})) }))
require.NoError(t, updateParams.SetNumActivePlayers(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetNumInactivePlayers(domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
}))
require.NoError(t, updateParams.SetPlayerDataSyncedAt(domain.NullTime{ require.NoError(t, updateParams.SetPlayerDataSyncedAt(domain.NullTime{
V: time.Now(), V: time.Now(),
Valid: true, Valid: true,
@ -534,6 +570,10 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
V: time.Now(), V: time.Now(),
Valid: true, Valid: true,
})) }))
require.NoError(t, updateParams.SetSnapshotCreatedAt(domain.NullTime{
V: time.Now(),
Valid: true,
}))
require.NoError(t, updateParams.SetTribeSnapshotsCreatedAt(domain.NullTime{ require.NoError(t, updateParams.SetTribeSnapshotsCreatedAt(domain.NullTime{
V: time.Now(), V: time.Now(),
Valid: true, Valid: true,
@ -557,6 +597,8 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
assert.Equal(t, updateParams.UnitInfo().V, serverAfterUpdate.UnitInfo()) assert.Equal(t, updateParams.UnitInfo().V, serverAfterUpdate.UnitInfo())
assert.Equal(t, updateParams.BuildingInfo().V, serverAfterUpdate.BuildingInfo()) assert.Equal(t, updateParams.BuildingInfo().V, serverAfterUpdate.BuildingInfo())
assert.Equal(t, updateParams.NumTribes().V, serverAfterUpdate.NumTribes()) assert.Equal(t, updateParams.NumTribes().V, serverAfterUpdate.NumTribes())
assert.Equal(t, updateParams.NumActiveTribes().V, serverAfterUpdate.NumActiveTribes())
assert.Equal(t, updateParams.NumInactiveTribes().V, serverAfterUpdate.NumInactiveTribes())
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.TribeDataSyncedAt().V, updateParams.TribeDataSyncedAt().V,
@ -564,6 +606,8 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
time.Minute, time.Minute,
) )
assert.Equal(t, updateParams.NumPlayers().V, serverAfterUpdate.NumPlayers()) assert.Equal(t, updateParams.NumPlayers().V, serverAfterUpdate.NumPlayers())
assert.Equal(t, updateParams.NumActivePlayers().V, serverAfterUpdate.NumActivePlayers())
assert.Equal(t, updateParams.NumInactivePlayers().V, serverAfterUpdate.NumInactivePlayers())
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.PlayerDataSyncedAt().V, updateParams.PlayerDataSyncedAt().V,
@ -586,6 +630,12 @@ func testServerRepository(t *testing.T, newRepos func(t *testing.T) repositories
serverAfterUpdate.EnnoblementDataSyncedAt(), serverAfterUpdate.EnnoblementDataSyncedAt(),
time.Minute, time.Minute,
) )
assert.WithinDuration(
t,
updateParams.SnapshotCreatedAt().V,
serverAfterUpdate.SnapshotCreatedAt(),
time.Minute,
)
assert.WithinDuration( assert.WithinDuration(
t, t,
updateParams.TribeSnapshotsCreatedAt().V, updateParams.TribeSnapshotsCreatedAt().V,

View File

@ -26,6 +26,7 @@ type tribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error
List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error) List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error)
Count(ctx context.Context, params domain.CountTribesParams) (int, error)
Delete(ctx context.Context, serverKey string, ids ...int) error Delete(ctx context.Context, serverKey string, ids ...int) error
} }
@ -33,6 +34,7 @@ type playerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error) List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error) ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error)
Count(ctx context.Context, params domain.CountPlayersParams) (int, error)
Delete(ctx context.Context, serverKey string, ids ...int) error Delete(ctx context.Context, serverKey string, ids ...int) error
} }
@ -65,6 +67,16 @@ type tribeChangeRepository interface {
) (domain.ListTribeChangesWithRelationsResult, error) ) (domain.ListTribeChangesWithRelationsResult, error)
} }
type serverSnapshotRepository interface {
Create(ctx context.Context, params ...domain.CreateServerSnapshotParams) error
List(ctx context.Context, params domain.ListServerSnapshotsParams) (domain.ListServerSnapshotsResult, error)
ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, error)
Delete(ctx context.Context, serverKey string, dateLTE time.Time) error
}
type tribeSnapshotRepository interface { type tribeSnapshotRepository interface {
Create(ctx context.Context, params ...domain.CreateTribeSnapshotParams) error Create(ctx context.Context, params ...domain.CreateTribeSnapshotParams) error
List(ctx context.Context, params domain.ListTribeSnapshotsParams) (domain.ListTribeSnapshotsResult, error) List(ctx context.Context, params domain.ListTribeSnapshotsParams) (domain.ListTribeSnapshotsResult, error)
@ -93,6 +105,7 @@ type repositories struct {
village villageRepository village villageRepository
ennoblement ennoblementRepository ennoblement ennoblementRepository
tribeChange tribeChangeRepository tribeChange tribeChangeRepository
serverSnapshot serverSnapshotRepository
tribeSnapshot tribeSnapshotRepository tribeSnapshot tribeSnapshotRepository
playerSnapshot playerSnapshotRepository playerSnapshot playerSnapshotRepository
} }
@ -110,6 +123,7 @@ func newBunDBRepositories(tb testing.TB, bunDB *bun.DB) repositories {
village: adapter.NewVillageBunRepository(bunDB), village: adapter.NewVillageBunRepository(bunDB),
ennoblement: adapter.NewEnnoblementBunRepository(bunDB), ennoblement: adapter.NewEnnoblementBunRepository(bunDB),
tribeChange: adapter.NewTribeChangeBunRepository(bunDB), tribeChange: adapter.NewTribeChangeBunRepository(bunDB),
serverSnapshot: adapter.NewServerSnapshotBunRepository(bunDB),
tribeSnapshot: adapter.NewTribeSnapshotBunRepository(bunDB), tribeSnapshot: adapter.NewTribeSnapshotBunRepository(bunDB),
playerSnapshot: adapter.NewPlayerSnapshotBunRepository(bunDB), playerSnapshot: adapter.NewPlayerSnapshotBunRepository(bunDB),
} }

View File

@ -73,12 +73,14 @@ func testTribeSnapshotRepository(t *testing.T, newRepos func(t *testing.T) repos
key := fmt.Sprintf("%s-%d-%s", p.ServerKey(), p.TribeID(), p.Date().Format(dateFormat)) key := fmt.Sprintf("%s-%d-%s", p.ServerKey(), p.TribeID(), p.Date().Format(dateFormat))
for i, ts := range tribeSnapshots { for i, ts := range tribeSnapshots {
if ts.ServerKey() == p.ServerKey() && ts.TribeID() == p.TribeID() && ts.Date().Equal(p.Date()) { //nolint:lll
if ts.ServerKey() == p.ServerKey() && ts.TribeID() == p.TribeID() && ts.Date().Format(dateFormat) == p.Date().Format(dateFormat) {
m[key] = append(m[key], i) m[key] = append(m[key], i)
} }
} }
} }
assert.NotEmpty(t, m)
for key, indexes := range m { for key, indexes := range m {
assert.Len(t, indexes, 1, key) assert.Len(t, indexes, 1, key)
} }

View File

@ -718,6 +718,78 @@ func testTribeRepository(t *testing.T, newRepos func(t *testing.T) repositories)
} }
}) })
t.Run("Count", func(t *testing.T) {
t.Parallel()
repos := newRepos(t)
tests := []struct {
name string
params func(t *testing.T) domain.CountTribesParams
assertResult func(t *testing.T, params domain.CountTribesParams, count int)
assertError func(t *testing.T, err error)
}{
{
name: "OK: default params",
params: func(t *testing.T) domain.CountTribesParams {
t.Helper()
return domain.NewCountTribesParams()
},
assertResult: func(t *testing.T, _ domain.CountTribesParams, count int) {
t.Helper()
res, err := repos.tribe.List(ctx, domain.NewListTribesParams())
require.NoError(t, err)
assert.Len(t, res.Tribes(), count)
},
},
{
name: "OK: serverKeys",
params: func(t *testing.T) domain.CountTribesParams {
t.Helper()
res, err := repos.tribe.List(ctx, domain.NewListTribesParams())
require.NoError(t, err)
params := domain.NewCountTribesParams()
require.NoError(t, params.SetServerKeys([]string{res.Tribes()[0].ServerKey()}))
return params
},
assertResult: func(t *testing.T, params domain.CountTribesParams, count int) {
t.Helper()
listParams := domain.NewListTribesParams()
require.NoError(t, listParams.SetServerKeys(params.ServerKeys()))
res, err := repos.tribe.List(ctx, listParams)
require.NoError(t, err)
assert.Len(t, res.Tribes(), count)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assertError := tt.assertError
if assertError == nil {
assertError = func(t *testing.T, err error) {
t.Helper()
require.NoError(t, err)
}
}
params := tt.params(t)
cnt, err := repos.tribe.Count(ctx, params)
assertError(t, err)
tt.assertResult(t, params, cnt)
})
}
})
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -5,76 +5,80 @@
url: https://de188.die-staemme.de url: https://de188.die-staemme.de
open: true open: true
special: false special: false
num_players: 180 num_active_players: 180
num_tribes: 76 num_active_tribes: 76
num_villages: 16180 num_villages: 16180
num_player_villages: 15000 num_player_villages: 15000
num_barbarian_villages: 1180 num_barbarian_villages: 1180
num_bonus_villages: 512 num_bonus_villages: 512
created_at: 2022-03-19T12:00:54.000Z created_at: 2022-03-19T12:00:54.000Z
player_data_updated_at: 2022-03-19T12:00:54.000Z snapshot_created_at: 2022-03-19T12:00:54.000Z
player_data_synced_at: 2022-03-19T12:00:54.000Z
player_snapshots_created_at: 2022-03-19T12:00:54.000Z player_snapshots_created_at: 2022-03-19T12:00:54.000Z
tribe_data_updated_at: 2022-03-19T12:00:54.000Z tribe_data_synced_at: 2022-03-19T12:00:54.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:54.000Z tribe_snapshots_created_at: 2022-03-19T12:00:54.000Z
village_data_updated_at: 2022-03-19T12:00:54.000Z village_data_synced_at: 2022-03-19T12:00:54.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:54.000Z ennoblement_data_synced_at: 2022-03-19T12:00:54.000Z
version_code: de version_code: de
- _id: en113 - _id: en113
key: en113 key: en113
url: https://en113.tribalwars.net url: https://en113.tribalwars.net
open: false open: false
special: false special: false
num_players: 251 num_active_players: 251
num_tribes: 57 num_active_tribes: 57
num_villages: 41700 num_villages: 41700
num_player_villages: 41000 num_player_villages: 41000
num_barbarian_villages: 700 num_barbarian_villages: 700
num_bonus_villages: 1024 num_bonus_villages: 1024
created_at: 2021-04-02T16:01:25.000Z created_at: 2021-04-02T16:01:25.000Z
player_data_updated_at: 2021-04-02T16:01:25.000Z snapshot_created_at: 2021-04-02T16:01:25.000Z
player_data_synced_at: 2021-04-02T16:01:25.000Z
player_snapshots_created_at: 2021-04-02T16:01:25.000Z player_snapshots_created_at: 2021-04-02T16:01:25.000Z
tribe_data_updated_at: 2021-04-02T16:01:25.000Z tribe_data_synced_at: 2021-04-02T16:01:25.000Z
tribe_snapshots_created_at: 2021-04-02T16:01:25.000Z tribe_snapshots_created_at: 2021-04-02T16:01:25.000Z
village_data_updated_at: 2021-04-02T16:01:25.000Z village_data_synced_at: 2021-04-02T16:01:25.000Z
ennoblement_data_updated_at: 2021-04-02T16:01:25.000Z ennoblement_data_synced_at: 2021-04-02T16:01:25.000Z
version_code: en version_code: en
- _id: it70 - _id: it70
key: it70 key: it70
url: https://it70.tribals.it url: https://it70.tribals.it
open: true open: true
special: false special: false
num_players: 1883 num_active_players: 1883
num_tribes: 101 num_active_tribes: 101
num_villages: 4882 num_villages: 4882
num_player_villages: 3200 num_player_villages: 3200
num_barbarian_villages: 1682 num_barbarian_villages: 1682
num_bonus_villages: 256 num_bonus_villages: 256
created_at: 2022-03-19T12:00:04.000Z created_at: 2022-03-19T12:00:04.000Z
player_data_updated_at: 2022-03-19T12:00:04.000Z snapshot_created_at: 2022-03-19T12:00:04.000Z
player_data_synced_at: 2022-03-19T12:00:04.000Z
player_snapshots_created_at: 2022-03-19T12:00:04.000Z player_snapshots_created_at: 2022-03-19T12:00:04.000Z
tribe_data_updated_at: 2022-03-19T12:00:04.000Z tribe_data_synced_at: 2022-03-19T12:00:04.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:04.000Z tribe_snapshots_created_at: 2022-03-19T12:00:04.000Z
village_data_updated_at: 2022-03-19T12:00:04.000Z village_data_synced_at: 2022-03-19T12:00:04.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:04.000Z ennoblement_data_synced_at: 2022-03-19T12:00:04.000Z
version_code: it version_code: it
- _id: pl169 - _id: pl169
key: pl169 key: pl169
url: https://pl169.plemiona.pl url: https://pl169.plemiona.pl
open: true open: true
special: false special: false
num_players: 2001 num_active_players: 2001
num_tribes: 214 num_active_tribes: 214
num_villages: 49074 num_villages: 49074
num_player_villages: 48500 num_player_villages: 48500
num_barbarian_villages: 1574 num_barbarian_villages: 1574
num_bonus_villages: 2048 num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z created_at: 2022-03-19T12:01:39.000Z
player_data_updated_at: 2022-03-19T12:01:39.000Z snapshot_created_at: 2022-03-19T12:01:39.000Z
player_data_synced_at: 2022-03-19T12:01:39.000Z
player_snapshots_created_at: 2022-03-19T12:01:39.000Z player_snapshots_created_at: 2022-03-19T12:01:39.000Z
tribe_data_updated_at: 2022-03-19T12:01:39.000Z tribe_data_synced_at: 2022-03-19T12:01:39.000Z
tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z
village_data_updated_at: 2022-03-19T12:01:39.000Z village_data_synced_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z ennoblement_data_synced_at: 2022-03-19T12:01:39.000Z
version_code: pl version_code: pl
- model: Tribe - model: Tribe
rows: rows:
@ -7374,6 +7378,64 @@
new_tribe_id: 2 new_tribe_id: 2
server_key: pl169 server_key: pl169
created_at: 2021-09-10T20:01:11.000Z created_at: 2021-09-10T20:01:11.000Z
- model: ServerSnapshot
rows:
- id: 10000
server_key: de188
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-01T05:15:25.154992Z
created_at: 2024-05-01T05:15:25.154994Z
- id: 10001
server_key: de188
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-02T05:15:25.154992Z
created_at: 2024-05-02T05:15:25.154994Z
- id: 20000
server_key: it70
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-03T05:15:25.154992Z
created_at: 2024-05-03T05:15:25.154994Z
- id: 20001
server_key: it70
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-04T05:15:25.154992Z
created_at: 2024-05-04T05:15:25.154994Z
- model: TribeSnapshot - model: TribeSnapshot
rows: rows:
- rank_att: 1 - rank_att: 1

View File

@ -11,6 +11,7 @@ type PlayerRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreatePlayerParams) error
List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error) List(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersResult, error)
ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error) ListWithRelations(ctx context.Context, params domain.ListPlayersParams) (domain.ListPlayersWithRelationsResult, error)
Count(ctx context.Context, params domain.CountPlayersParams) (int, error)
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now). // Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
// In addition, Delete sets TribeID to null. // In addition, Delete sets TribeID to null.
// //
@ -51,10 +52,21 @@ func (svc *PlayerService) Sync(ctx context.Context, serverSyncedPayload domain.S
return fmt.Errorf("%s: couldn't delete players: %w", serverKey, err) return fmt.Errorf("%s: couldn't delete players: %w", serverKey, err)
} }
countParams := domain.NewCountPlayersParams()
if err = countParams.SetServerKeys([]string{serverKey}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
numPlayers, err := svc.repo.Count(ctx, countParams)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
payload, err := domain.NewPlayersSyncedEventPayload( payload, err := domain.NewPlayersSyncedEventPayload(
serverKey, serverKey,
serverURL, serverURL,
serverSyncedPayload.VersionCode(), serverSyncedPayload.VersionCode(),
numPlayers,
len(players), len(players),
) )
if err != nil { if err != nil {
@ -101,7 +113,7 @@ func (svc *PlayerService) createOrUpdateChunk(ctx context.Context, serverKey str
if err := listParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil { if err := listParams.SetSort([]domain.PlayerSort{domain.PlayerSortIDASC}); err != nil {
return err return err
} }
if err := listParams.SetLimit(domain.PlayerListMaxLimit); err != nil { if err := listParams.SetLimit(len(ids)); err != nil {
return err return err
} }

View File

@ -189,6 +189,18 @@ func (svc *ServerService) UpdateNumTribes(ctx context.Context, payload domain.Tr
}); err != nil { }); err != nil {
return fmt.Errorf("%s: %w", key, err) return fmt.Errorf("%s: %w", key, err)
} }
if err := updateParams.SetNumActiveTribes(domain.NullInt{
V: payload.NumActiveTribes(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetNumInactiveTribes(domain.NullInt{
V: payload.NumInactiveTribes(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetTribeDataSyncedAt(domain.NullTime{ if err := updateParams.SetTribeDataSyncedAt(domain.NullTime{
V: time.Now(), V: time.Now(),
Valid: true, Valid: true,
@ -209,6 +221,18 @@ func (svc *ServerService) UpdateNumPlayers(ctx context.Context, payload domain.P
}); err != nil { }); err != nil {
return fmt.Errorf("%s: %w", key, err) return fmt.Errorf("%s: %w", key, err)
} }
if err := updateParams.SetNumActivePlayers(domain.NullInt{
V: payload.NumActivePlayers(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetNumInactivePlayers(domain.NullInt{
V: payload.NumInactivePlayers(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
if err := updateParams.SetPlayerDataSyncedAt(domain.NullTime{ if err := updateParams.SetPlayerDataSyncedAt(domain.NullTime{
V: time.Now(), V: time.Now(),
Valid: true, Valid: true,
@ -274,6 +298,21 @@ func (svc *ServerService) UpdateEnnoblementDataSyncedAt(
return svc.repo.Update(ctx, key, updateParams) return svc.repo.Update(ctx, key, updateParams)
} }
func (svc *ServerService) UpdateSnapshotCreatedAt(
ctx context.Context,
key string,
) error {
var updateParams domain.UpdateServerParams
if err := updateParams.SetSnapshotCreatedAt(domain.NullTime{
V: time.Now(),
Valid: true,
}); err != nil {
return fmt.Errorf("%s: %w", key, err)
}
return svc.repo.Update(ctx, key, updateParams)
}
func (svc *ServerService) UpdateTribeSnapshotsCreatedAt( func (svc *ServerService) UpdateTribeSnapshotsCreatedAt(
ctx context.Context, ctx context.Context,
payload domain.SnapshotsCreatedEventPayload, payload domain.SnapshotsCreatedEventPayload,

View File

@ -0,0 +1,72 @@
package app
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
)
type ServerSnapshotRepository interface {
// Create persists tribe snapshots in a store (e.g. Postgres).
// Duplicates are ignored.
Create(ctx context.Context, params ...domain.CreateServerSnapshotParams) error
ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, error)
}
type ServerSnapshotService struct {
repo ServerSnapshotRepository
serverSvc *ServerService
pub SnapshotPublisher
}
func NewServerSnapshotService(
repo ServerSnapshotRepository,
serverSvc *ServerService,
pub SnapshotPublisher,
) *ServerSnapshotService {
return &ServerSnapshotService{repo: repo, serverSvc: serverSvc, pub: pub}
}
func (svc *ServerSnapshotService) Create(
ctx context.Context,
createSnapshotsCmdPayload domain.CreateSnapshotsCmdPayload,
) error {
versionCode := createSnapshotsCmdPayload.VersionCode()
serverKey := createSnapshotsCmdPayload.ServerKey()
date := createSnapshotsCmdPayload.Date()
server, err := svc.serverSvc.GetNormalByVersionCodeAndServerKey(ctx, versionCode, serverKey)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
if !server.Open() {
return nil
}
params, err := domain.NewCreateServerSnapshotParams(server, 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 = svc.serverSvc.UpdateSnapshotCreatedAt(ctx, serverKey); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
return nil
}
func (svc *ServerSnapshotService) ListWithRelations(
ctx context.Context,
params domain.ListServerSnapshotsParams,
) (domain.ListServerSnapshotsWithRelationsResult, error) {
return svc.repo.ListWithRelations(ctx, params)
}

View File

@ -12,6 +12,7 @@ import (
type SnapshotService struct { type SnapshotService struct {
versionSvc *VersionService versionSvc *VersionService
serverSvc *ServerService serverSvc *ServerService
serverSnapshotPub SnapshotPublisher
tribeSnapshotPub SnapshotPublisher tribeSnapshotPub SnapshotPublisher
playerSnapshotPub SnapshotPublisher playerSnapshotPub SnapshotPublisher
} }
@ -19,14 +20,16 @@ type SnapshotService struct {
func NewSnapshotService( func NewSnapshotService(
versionSvc *VersionService, versionSvc *VersionService,
serverSvc *ServerService, serverSvc *ServerService,
tribeSnapshotPublisher SnapshotPublisher, serverSnapshotPub SnapshotPublisher,
playerSnapshotPublisher SnapshotPublisher, tribeSnapshotPub SnapshotPublisher,
playerSnapshotPub SnapshotPublisher,
) *SnapshotService { ) *SnapshotService {
return &SnapshotService{ return &SnapshotService{
versionSvc: versionSvc, versionSvc: versionSvc,
serverSvc: serverSvc, serverSvc: serverSvc,
tribeSnapshotPub: tribeSnapshotPublisher, serverSnapshotPub: serverSnapshotPub,
playerSnapshotPub: playerSnapshotPublisher, tribeSnapshotPub: tribeSnapshotPub,
playerSnapshotPub: playerSnapshotPub,
} }
} }
@ -48,6 +51,7 @@ func (svc *SnapshotService) Create(ctx context.Context) error {
date := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) date := time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
if loopErr = errors.Join( if loopErr = errors.Join(
svc.publishServer(ctx, v, snapshotsCreatedAtLT, date),
svc.publishTribe(ctx, v, snapshotsCreatedAtLT, date), svc.publishTribe(ctx, v, snapshotsCreatedAtLT, date),
svc.publishPlayer(ctx, v, snapshotsCreatedAtLT, date), svc.publishPlayer(ctx, v, snapshotsCreatedAtLT, date),
); loopErr != nil { ); loopErr != nil {
@ -58,23 +62,47 @@ func (svc *SnapshotService) Create(ctx context.Context) error {
return nil return nil
} }
func (svc *SnapshotService) publishServer(
ctx context.Context,
v domain.Version,
snapshotCreatedAtLT time.Time,
date time.Time,
) error {
params, err := svc.baseParams(v)
if err != nil {
return err
}
if err = params.SetSnapshotCreatedAtLT(domain.NullTime{
V: snapshotCreatedAtLT,
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.serverSnapshotPub.CmdCreate(ctx, payloads...)
}
func (svc *SnapshotService) publishTribe( func (svc *SnapshotService) publishTribe(
ctx context.Context, ctx context.Context,
v domain.Version, v domain.Version,
snapshotsCreatedAtLT time.Time, snapshotsCreatedAtLT time.Time,
date time.Time, date time.Time,
) error { ) error {
params := domain.NewListServersParams() params, err := svc.baseParams(v)
if err := params.SetVersionCodes([]string{v.Code()}); err != nil { if err != nil {
return err return err
} }
if err := params.SetOpen(domain.NullBool{ if err = params.SetTribeSnapshotsCreatedAtLT(domain.NullTime{
V: true,
Valid: true,
}); err != nil {
return err
}
if err := params.SetTribeSnapshotsCreatedAtLT(domain.NullTime{
V: snapshotsCreatedAtLT, V: snapshotsCreatedAtLT,
Valid: true, Valid: true,
}); err != nil { }); err != nil {
@ -100,17 +128,11 @@ func (svc *SnapshotService) publishPlayer(
snapshotsCreatedAtLT time.Time, snapshotsCreatedAtLT time.Time,
date time.Time, date time.Time,
) error { ) error {
params := domain.NewListServersParams() params, err := svc.baseParams(v)
if err := params.SetVersionCodes([]string{v.Code()}); err != nil { if err != nil {
return err return err
} }
if err := params.SetOpen(domain.NullBool{ if err = params.SetPlayerSnapshotsCreatedAtLT(domain.NullTime{
V: true,
Valid: true,
}); err != nil {
return err
}
if err := params.SetPlayerSnapshotsCreatedAtLT(domain.NullTime{
V: snapshotsCreatedAtLT, V: snapshotsCreatedAtLT,
Valid: true, Valid: true,
}); err != nil { }); err != nil {
@ -130,6 +152,20 @@ func (svc *SnapshotService) publishPlayer(
return svc.playerSnapshotPub.CmdCreate(ctx, payloads...) return svc.playerSnapshotPub.CmdCreate(ctx, payloads...)
} }
func (svc *SnapshotService) baseParams(v domain.Version) (domain.ListServersParams, error) {
params := domain.NewListServersParams()
if err := params.SetVersionCodes([]string{v.Code()}); err != nil {
return domain.ListServersParams{}, err
}
if err := params.SetOpen(domain.NullBool{
V: true,
Valid: true,
}); err != nil {
return domain.ListServersParams{}, nil
}
return params, nil
}
func (svc *SnapshotService) toPayload( func (svc *SnapshotService) toPayload(
v domain.Version, v domain.Version,
servers domain.Servers, servers domain.Servers,

View File

@ -11,6 +11,7 @@ type TribeRepository interface {
CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error CreateOrUpdate(ctx context.Context, params ...domain.CreateTribeParams) error
UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error UpdateDominance(ctx context.Context, serverKey string, numPlayerVillages int) error
List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error) List(ctx context.Context, params domain.ListTribesParams) (domain.ListTribesResult, error)
Count(ctx context.Context, params domain.CountTribesParams) (int, error)
// Delete marks players with the given serverKey and ids as deleted (sets deleted at to now). // Delete marks players with the given serverKey and ids as deleted (sets deleted at to now).
// //
// https://en.wiktionary.org/wiki/soft_deletion // https://en.wiktionary.org/wiki/soft_deletion
@ -44,10 +45,21 @@ func (svc *TribeService) Sync(ctx context.Context, serverSyncedPayload domain.Se
return fmt.Errorf("%s: couldn't delete tribes: %w", serverKey, err) return fmt.Errorf("%s: couldn't delete tribes: %w", serverKey, err)
} }
countParams := domain.NewCountTribesParams()
if err = countParams.SetServerKeys([]string{serverKey}); err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
numTribes, err := svc.repo.Count(ctx, countParams)
if err != nil {
return fmt.Errorf("%s: %w", serverKey, err)
}
payload, err := domain.NewTribesSyncedEventPayload( payload, err := domain.NewTribesSyncedEventPayload(
serverKey, serverKey,
serverURL, serverURL,
serverSyncedPayload.VersionCode(), serverSyncedPayload.VersionCode(),
numTribes,
len(tribes), len(tribes),
) )
if err != nil { if err != nil {
@ -94,7 +106,7 @@ func (svc *TribeService) createOrUpdateChunk(ctx context.Context, serverKey stri
if err := listParams.SetSort([]domain.TribeSort{domain.TribeSortIDASC}); err != nil { if err := listParams.SetSort([]domain.TribeSort{domain.TribeSortIDASC}); err != nil {
return err return err
} }
if err := listParams.SetLimit(domain.TribeListMaxLimit); err != nil { if err := listParams.SetLimit(len(ids)); err != nil {
return err return err
} }

View File

@ -11,32 +11,33 @@ import (
var ServerMetaColumns = []string{"key", "url", "open"} var ServerMetaColumns = []string{"key", "url", "open"}
type Server struct { type Server struct {
bun.BaseModel `bun:"table:servers,alias:server"` bun.BaseModel `bun:"table:servers,alias:server"`
Key string `bun:"key,nullzero,pk"` Key string `bun:"key,nullzero,pk"`
URL string `bun:"url,nullzero"` URL string `bun:"url,nullzero"`
Open bool `bun:"open"` Open bool `bun:"open"`
Special bool `bun:"special"` Special bool `bun:"special"`
NumPlayers int `bun:"num_players"` NumPlayers int `bun:"num_players"`
NumTribes int `bun:"num_tribes"` NumActivePlayers int `bun:"num_active_players"`
NumVillages int `bun:"num_villages"` NumInactivePlayers int `bun:"num_inactive_players"`
NumPlayerVillages int `bun:"num_player_villages"` NumTribes int `bun:"num_tribes"`
NumBarbarianVillages int `bun:"num_barbarian_villages"` NumActiveTribes int `bun:"num_active_tribes"`
NumBonusVillages int `bun:"num_bonus_villages"` NumInactiveTribes int `bun:"num_inactive_tribes"`
Config ServerConfig `bun:"config"` NumVillages int `bun:"num_villages"`
BuildingInfo BuildingInfo `bun:"building_info"` NumPlayerVillages int `bun:"num_player_villages"`
UnitInfo UnitInfo `bun:"unit_info"` NumBarbarianVillages int `bun:"num_barbarian_villages"`
CreatedAt time.Time `bun:"created_at,nullzero"` NumBonusVillages int `bun:"num_bonus_villages"`
// TODO: rename this column to player_data_synced_at Config ServerConfig `bun:"config"`
PlayerDataUpdatedAt time.Time `bun:"player_data_updated_at,nullzero"` BuildingInfo BuildingInfo `bun:"building_info"`
PlayerSnapshotsCreatedAt time.Time `bun:"player_snapshots_created_at,nullzero"` UnitInfo UnitInfo `bun:"unit_info"`
// TODO: rename this column to tribe_data_synced_at CreatedAt time.Time `bun:"created_at,nullzero"`
TribeDataUpdatedAt time.Time `bun:"tribe_data_updated_at,nullzero"` SnapshotCreatedAt time.Time `bun:"snapshot_created_at,nullzero"`
TribeSnapshotsCreatedAt time.Time `bun:"tribe_snapshots_created_at,nullzero"` PlayerDataUpdatedAt time.Time `bun:"player_data_synced_at,nullzero"`
// TODO: rename this column to village_data_synced_at PlayerSnapshotsCreatedAt time.Time `bun:"player_snapshots_created_at,nullzero"`
VillageDataUpdatedAt time.Time `bun:"village_data_updated_at,nullzero"` TribeDataUpdatedAt time.Time `bun:"tribe_data_synced_at,nullzero"`
// TODO: rename this column to ennoblement_data_synced_at TribeSnapshotsCreatedAt time.Time `bun:"tribe_snapshots_created_at,nullzero"`
EnnoblementDataUpdatedAt time.Time `bun:"ennoblement_data_updated_at,nullzero"` VillageDataUpdatedAt time.Time `bun:"village_data_synced_at,nullzero"`
VersionCode string `bun:"version_code,nullzero"` EnnoblementDataUpdatedAt time.Time `bun:"ennoblement_data_synced_at,nullzero"`
VersionCode string `bun:"version_code,nullzero"`
} }
func (s Server) ToDomain() (domain.Server, error) { func (s Server) ToDomain() (domain.Server, error) {
@ -62,7 +63,11 @@ func (s Server) ToDomain() (domain.Server, error) {
s.Open, s.Open,
s.Special, s.Special,
s.NumPlayers, s.NumPlayers,
s.NumActivePlayers,
s.NumInactivePlayers,
s.NumTribes, s.NumTribes,
s.NumActiveTribes,
s.NumInactiveTribes,
s.NumVillages, s.NumVillages,
s.NumPlayerVillages, s.NumPlayerVillages,
s.NumBarbarianVillages, s.NumBarbarianVillages,
@ -71,6 +76,7 @@ func (s Server) ToDomain() (domain.Server, error) {
buildingInfo, buildingInfo,
unitInfo, unitInfo,
s.CreatedAt, s.CreatedAt,
s.SnapshotCreatedAt,
s.PlayerDataUpdatedAt, s.PlayerDataUpdatedAt,
s.PlayerSnapshotsCreatedAt, s.PlayerSnapshotsCreatedAt,
s.TribeDataUpdatedAt, s.TribeDataUpdatedAt,
@ -88,7 +94,7 @@ func (s Server) ToDomain() (domain.Server, error) {
func (s Server) ToMeta() (domain.ServerMeta, error) { func (s Server) ToMeta() (domain.ServerMeta, error) {
converted, err := domain.UnmarshalServerMetaFromDatabase(s.Key, s.URL, s.Open) converted, err := domain.UnmarshalServerMetaFromDatabase(s.Key, s.URL, s.Open)
if err != nil { if err != nil {
return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.Server (key=%s): %w", s.Key, err) return domain.ServerMeta{}, fmt.Errorf("couldn't construct domain.ServerMeta (key=%s): %w", s.Key, err)
} }
return converted, nil return converted, nil
} }

View File

@ -0,0 +1,81 @@
package bunmodel
import (
"fmt"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/uptrace/bun"
)
type ServerSnapshot struct {
bun.BaseModel `bun:"table:server_snapshots,alias:ss"`
ID int `bun:"id,pk,autoincrement,identity"`
ServerKey string `bun:"server_key,nullzero"`
Server Server `bun:"server,rel:belongs-to,join:server_key=key"`
NumPlayers int `bun:"num_players"`
NumActivePlayers int `bun:"num_active_players"`
NumInactivePlayers int `bun:"num_inactive_players"`
NumTribes int `bun:"num_tribes"`
NumActiveTribes int `bun:"num_active_tribes"`
NumInactiveTribes int `bun:"num_inactive_tribes"`
NumVillages int `bun:"num_villages"`
NumPlayerVillages int `bun:"num_player_villages"`
NumBarbarianVillages int `bun:"num_barbarian_villages"`
NumBonusVillages int `bun:"num_bonus_villages"`
Date time.Time `bun:"date,nullzero"`
CreatedAt time.Time `bun:"created_at,nullzero"`
}
func (ss ServerSnapshot) ToDomain() (domain.ServerSnapshot, error) {
converted, err := domain.UnmarshalServerSnapshotFromDatabase(
ss.ID,
ss.ServerKey,
ss.NumPlayers,
ss.NumActivePlayers,
ss.NumInactivePlayers,
ss.NumTribes,
ss.NumActiveTribes,
ss.NumInactiveTribes,
ss.NumVillages,
ss.NumPlayerVillages,
ss.NumBarbarianVillages,
ss.NumBonusVillages,
ss.Date,
ss.CreatedAt,
)
if err != nil {
return domain.ServerSnapshot{}, fmt.Errorf(
"couldn't construct domain.ServerSnapshot (id=%d): %w",
ss.ID,
err,
)
}
return converted, nil
}
func (ss ServerSnapshot) ToDomainWithRelations() (domain.ServerSnapshotWithRelations, error) {
converted, err := ss.ToDomain()
if err != nil {
return domain.ServerSnapshotWithRelations{}, err
}
server, err := ss.Server.ToMeta()
if err != nil {
return domain.ServerSnapshotWithRelations{}, err
}
return converted.WithRelations(server), nil
}
type ServerSnapshots []ServerSnapshot
func (sss ServerSnapshots) ToDomain() (domain.ServerSnapshots, error) {
return sliceToDomain(sss)
}
func (sss ServerSnapshots) ToDomainWithRelations() (domain.ServerSnapshotsWithRelations, error) {
return sliceToDomainWithRelations(sss)
}

View File

@ -23,6 +23,7 @@ func NewFixture(bunDB *bun.DB) *Fixture {
(*bunmodel.Village)(nil), (*bunmodel.Village)(nil),
(*bunmodel.Ennoblement)(nil), (*bunmodel.Ennoblement)(nil),
(*bunmodel.TribeChange)(nil), (*bunmodel.TribeChange)(nil),
(*bunmodel.ServerSnapshot)(nil),
(*bunmodel.TribeSnapshot)(nil), (*bunmodel.TribeSnapshot)(nil),
(*bunmodel.PlayerSnapshot)(nil), (*bunmodel.PlayerSnapshot)(nil),
) )

View File

@ -0,0 +1,44 @@
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240504051104() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(oldColumn), bun.Safe(newColumn))
if err != nil {
return fmt.Errorf("%s: %w", oldColumn, err)
}
}
return nil
})
}, func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240504051104() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(newColumn), bun.Safe(oldColumn))
if err != nil {
return fmt.Errorf("%s: %w", newColumn, err)
}
}
return nil
})
})
}
func getColumnsToRename20240504051104() map[string]string {
return map[string]string{
"player_data_updated_at": "player_data_synced_at",
"tribe_data_updated_at": "tribe_data_synced_at",
"village_data_updated_at": "village_data_synced_at",
"ennoblement_data_updated_at": "ennoblement_data_synced_at",
}
}

View File

@ -0,0 +1,42 @@
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240506043013() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(oldColumn), bun.Safe(newColumn))
if err != nil {
return fmt.Errorf("%s: %w", oldColumn, err)
}
}
return nil
})
}, func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for oldColumn, newColumn := range getColumnsToRename20240506043013() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers RENAME COLUMN ? TO ?", bun.Safe(newColumn), bun.Safe(oldColumn))
if err != nil {
return fmt.Errorf("%s: %w", newColumn, err)
}
}
return nil
})
})
}
func getColumnsToRename20240506043013() map[string]string {
return map[string]string{
"num_players": "num_active_players",
"num_tribes": "num_active_tribes",
}
}

View File

@ -0,0 +1,44 @@
package migrations
import (
"context"
"database/sql"
"fmt"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for _, column := range getColumnsToAdd20240506051128() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers ADD ? bigint DEFAULT 0", bun.Safe(column))
if err != nil {
return fmt.Errorf("%s: %w", column, err)
}
}
return nil
})
}, func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error {
for _, column := range getColumnsToAdd20240506051128() {
_, err := tx.ExecContext(ctx, "ALTER TABLE servers DROP COLUMN ?", bun.Safe(column))
if err != nil {
return fmt.Errorf("%s: %w", column, err)
}
}
return nil
})
})
}
func getColumnsToAdd20240506051128() []string {
return []string{
"num_players",
"num_inactive_players",
"num_tribes",
"num_inactive_tribes",
}
}

View File

@ -0,0 +1,52 @@
package migrations
import (
"context"
"fmt"
"gitea.dwysokinski.me/twhelp/core/internal/bun/bunmodel"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
var servers bunmodel.Servers
if err := db.NewSelect().
Model(&servers).
ExcludeColumn("snapshot_created_at").
Where("special = false").
Scan(ctx); err != nil {
return fmt.Errorf("couldn't select servers from the db: %w", err)
}
for _, s := range servers {
numPlayers, err := db.NewSelect().Model((*bunmodel.Player)(nil)).Where("server_key = ?", s.Key).Count(ctx)
if err != nil {
return fmt.Errorf("%s: numPlayers: %w", s.Key, err)
}
numTribes, err := db.NewSelect().Model((*bunmodel.Tribe)(nil)).Where("server_key = ?", s.Key).Count(ctx)
if err != nil {
return fmt.Errorf("%s: numTribes: %w", s.Key, err)
}
_, err = db.NewUpdate().
Model((*bunmodel.Server)(nil)).
Returning("NULL").
Where("key = ?", s.Key).
Set("num_players = ?", numPlayers).
Set("num_inactive_players = ?", numPlayers-s.NumActivePlayers).
Set("num_tribes = ?", numTribes).
Set("num_inactive_tribes = ?", numTribes-s.NumActiveTribes).
Exec(ctx)
if err != nil {
return fmt.Errorf("%s: %w", s.Key, err)
}
}
return nil
}, func(_ context.Context, _ *bun.DB) error {
return nil
})
}

View File

@ -0,0 +1,17 @@
package migrations
import (
"context"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE servers ADD snapshot_created_at timestamp with time zone")
return err
}, func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "ALTER TABLE servers DROP COLUMN snapshot_created_at")
return err
})
}

View File

@ -0,0 +1,37 @@
package migrations
import (
"context"
"github.com/uptrace/bun"
)
func init() {
migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, `
create table if not exists server_snapshots
(
?ID_COL,
server_key varchar(100) not null
references servers,
date date not null,
created_at timestamp with time zone default CURRENT_TIMESTAMP not null,
num_players bigint default 0,
num_active_players bigint default 0,
num_inactive_players bigint default 0,
num_tribes bigint default 0,
num_active_tribes bigint default 0,
num_inactive_tribes bigint default 0,
num_villages bigint default 0,
num_player_villages bigint default 0,
num_barbarian_villages bigint default 0,
num_bonus_villages bigint default 0,
unique (server_key, date)
);
`)
return err
}, func(ctx context.Context, db *bun.DB) error {
_, err := db.ExecContext(ctx, "drop table if exists server_snapshots cascade;")
return err
})
}

View File

@ -47,7 +47,7 @@ func Logger(logger Slogger, opts ...Option) func(next http.Handler) http.Handler
slog.String("httpRequest.ip", cfg.ipExtractor(r)), slog.String("httpRequest.ip", cfg.ipExtractor(r)),
slog.Int("httpResponse.status", status), slog.Int("httpResponse.status", status),
slog.Int("httpResponse.bytes", ww.BytesWritten()), slog.Int("httpResponse.bytes", ww.BytesWritten()),
//nolint:gomnd //nolint:mnd
slog.Float64("httpResponse.duration", float64(end.Sub(start).Nanoseconds())/1000000.0), // in milliseconds slog.Float64("httpResponse.duration", float64(end.Sub(start).Nanoseconds())/1000000.0), // in milliseconds
) )
}) })

View File

@ -83,16 +83,21 @@ func NewServer(tb TestingTB, opts ...func(cfg *ServerConfig)) domain.Server {
cfg.URL.String(), cfg.URL.String(),
cfg.Open, cfg.Open,
cfg.Special, cfg.Special,
0, gofakeit.IntRange(0, 100000),
0, gofakeit.IntRange(0, 100000),
0, gofakeit.IntRange(0, 100000),
0, gofakeit.IntRange(0, 100000),
0, gofakeit.IntRange(0, 100000),
0, gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
gofakeit.IntRange(0, 100000),
NewServerConfig(tb), NewServerConfig(tb),
NewBuildingInfo(tb), NewBuildingInfo(tb),
NewUnitInfo(tb), NewUnitInfo(tb),
time.Now(), time.Now(),
time.Now(),
cfg.PlayerDataSyncedAt, cfg.PlayerDataSyncedAt,
cfg.PlayerSnapshotsCreatedAt, cfg.PlayerSnapshotsCreatedAt,
cfg.TribeDataSyncedAt, cfg.TribeDataSyncedAt,

View File

@ -0,0 +1,137 @@
package domaintest
import (
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/require"
)
type ServerSnapshotCursorConfig struct {
ID int
ServerKey string
Date time.Time
}
func NewServerSnapshotCursor(tb TestingTB, opts ...func(cfg *ServerSnapshotCursorConfig)) domain.ServerSnapshotCursor {
tb.Helper()
cfg := &ServerSnapshotCursorConfig{
ID: RandID(),
ServerKey: RandServerKey(),
Date: gofakeit.Date(),
}
for _, opt := range opts {
opt(cfg)
}
ssc, err := domain.NewServerSnapshotCursor(
cfg.ID,
cfg.ServerKey,
cfg.Date,
)
require.NoError(tb, err)
return ssc
}
type ServerSnapshotConfig struct {
ID int
ServerKey string
NumPlayers int
NumActivePlayers int
NumInactivePlayers int
NumTribes int
NumActiveTribes int
NumInactiveTribes int
NumVillages int
NumPlayerVillages int
NumBarbarianVillages int
NumBonusVillages int
Date time.Time
CreatedAt time.Time
}
func NewServerSnapshot(tb TestingTB, opts ...func(cfg *ServerSnapshotConfig)) domain.ServerSnapshot {
tb.Helper()
now := time.Now()
cfg := &ServerSnapshotConfig{
ID: RandID(),
ServerKey: RandServerKey(),
NumPlayers: gofakeit.IntRange(1, 10000),
NumActivePlayers: gofakeit.IntRange(1, 10000),
NumInactivePlayers: gofakeit.IntRange(1, 10000),
NumTribes: gofakeit.IntRange(1, 10000),
NumActiveTribes: gofakeit.IntRange(1, 10000),
NumInactiveTribes: gofakeit.IntRange(1, 10000),
NumVillages: gofakeit.IntRange(1, 10000),
NumPlayerVillages: gofakeit.IntRange(1, 10000),
NumBarbarianVillages: gofakeit.IntRange(1, 10000),
NumBonusVillages: gofakeit.IntRange(1, 10000),
Date: now,
CreatedAt: now,
}
for _, opt := range opts {
opt(cfg)
}
ss, err := domain.UnmarshalServerSnapshotFromDatabase(
cfg.ID,
cfg.ServerKey,
cfg.NumPlayers,
cfg.NumActivePlayers,
cfg.NumInactivePlayers,
cfg.NumTribes,
cfg.NumActiveTribes,
cfg.NumInactiveTribes,
cfg.NumVillages,
cfg.NumPlayerVillages,
cfg.NumBarbarianVillages,
cfg.NumBonusVillages,
cfg.Date,
cfg.CreatedAt,
)
require.NoError(tb, err)
return ss
}
type ServerSnapshotWithRelationsConfig struct {
ServerSnapshotOptions []func(cfg *ServerSnapshotConfig)
ServerOptions []func(cfg *ServerConfig)
}
func NewServerSnapshotWithRelations(
tb TestingTB,
opts ...func(cfg *ServerSnapshotWithRelationsConfig),
) domain.ServerSnapshotWithRelations {
tb.Helper()
cfg := &ServerSnapshotWithRelationsConfig{}
for _, opt := range opts {
opt(cfg)
}
ss := NewServerSnapshot(tb, cfg.ServerSnapshotOptions...)
if ss.ServerKey() != "" {
cfg.ServerOptions = append([]func(cfg *ServerConfig){
func(cfg *ServerConfig) {
cfg.Key = ss.ServerKey()
},
}, cfg.ServerOptions...)
}
var tribe domain.ServerMeta
if len(cfg.ServerOptions) > 0 {
tribe = NewServer(tb, cfg.ServerOptions...).Meta()
}
return ss.WithRelations(tribe)
}

View File

@ -27,14 +27,14 @@ func NewTribeSnapshotCursor(tb TestingTB, opts ...func(cfg *TribeSnapshotCursorC
opt(cfg) opt(cfg)
} }
psc, err := domain.NewTribeSnapshotCursor( tsc, err := domain.NewTribeSnapshotCursor(
cfg.ID, cfg.ID,
cfg.ServerKey, cfg.ServerKey,
cfg.Date, cfg.Date,
) )
require.NoError(tb, err) require.NoError(tb, err)
return psc return tsc
} }
type TribeSnapshotConfig struct { type TribeSnapshotConfig struct {

View File

@ -6,6 +6,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func RandVersionCode() string {
return gofakeit.LetterN(2)
}
type VersionCursorConfig struct { type VersionCursorConfig struct {
Code string Code string
} }
@ -52,7 +56,3 @@ func NewVersion(tb TestingTB, opts ...func(cfg *VersionConfig)) domain.Version {
return v return v
} }
func RandVersionCode() string {
return gofakeit.LetterN(2)
}

View File

@ -109,10 +109,11 @@ func (p ServerSyncedEventPayload) VersionCode() string {
} }
type TribesSyncedEventPayload struct { type TribesSyncedEventPayload struct {
serverKey string serverKey string
serverURL *url.URL serverURL *url.URL
versionCode string versionCode string
numTribes int numTribes int
numActiveTribes int
} }
const tribesSyncedEventPayloadModelName = "TribesSyncedEventPayload" const tribesSyncedEventPayloadModelName = "TribesSyncedEventPayload"
@ -122,6 +123,7 @@ func NewTribesSyncedEventPayload(
serverURL *url.URL, serverURL *url.URL,
versionCode string, versionCode string,
numTribes int, numTribes int,
numActiveTribes int,
) (TribesSyncedEventPayload, error) { ) (TribesSyncedEventPayload, error) {
if serverKey == "" { if serverKey == "" {
return TribesSyncedEventPayload{}, ValidationError{ return TribesSyncedEventPayload{}, ValidationError{
@ -155,11 +157,20 @@ func NewTribesSyncedEventPayload(
} }
} }
if err := validateIntInRange(numActiveTribes, 0, math.MaxInt); err != nil {
return TribesSyncedEventPayload{}, ValidationError{
Model: tribesSyncedEventPayloadModelName,
Field: "numActiveTribes",
Err: err,
}
}
return TribesSyncedEventPayload{ return TribesSyncedEventPayload{
serverKey: serverKey, serverKey: serverKey,
serverURL: serverURL, serverURL: serverURL,
versionCode: versionCode, versionCode: versionCode,
numTribes: numTribes, numTribes: numTribes,
numActiveTribes: numActiveTribes,
}, nil }, nil
} }
@ -179,11 +190,20 @@ func (p TribesSyncedEventPayload) NumTribes() int {
return p.numTribes return p.numTribes
} }
func (p TribesSyncedEventPayload) NumActiveTribes() int {
return p.numActiveTribes
}
func (p TribesSyncedEventPayload) NumInactiveTribes() int {
return p.numTribes - p.numActiveTribes
}
type PlayersSyncedEventPayload struct { type PlayersSyncedEventPayload struct {
serverKey string serverKey string
serverURL *url.URL serverURL *url.URL
versionCode string versionCode string
numPlayers int numPlayers int
numActivePlayers int
} }
const playersSyncedEventPayloadModelName = "PlayersSyncedEventPayload" const playersSyncedEventPayloadModelName = "PlayersSyncedEventPayload"
@ -193,6 +213,7 @@ func NewPlayersSyncedEventPayload(
serverURL *url.URL, serverURL *url.URL,
versionCode string, versionCode string,
numPlayers int, numPlayers int,
numActivePlayers int,
) (PlayersSyncedEventPayload, error) { ) (PlayersSyncedEventPayload, error) {
if serverKey == "" { if serverKey == "" {
return PlayersSyncedEventPayload{}, ValidationError{ return PlayersSyncedEventPayload{}, ValidationError{
@ -226,11 +247,20 @@ func NewPlayersSyncedEventPayload(
} }
} }
if err := validateIntInRange(numActivePlayers, 0, math.MaxInt); err != nil {
return PlayersSyncedEventPayload{}, ValidationError{
Model: playersSyncedEventPayloadModelName,
Field: "numActivePlayers",
Err: err,
}
}
return PlayersSyncedEventPayload{ return PlayersSyncedEventPayload{
serverKey: serverKey, serverKey: serverKey,
serverURL: serverURL, serverURL: serverURL,
versionCode: versionCode, versionCode: versionCode,
numPlayers: numPlayers, numPlayers: numPlayers,
numActivePlayers: numActivePlayers,
}, nil }, nil
} }
@ -250,6 +280,14 @@ func (p PlayersSyncedEventPayload) NumPlayers() int {
return p.numPlayers return p.numPlayers
} }
func (p PlayersSyncedEventPayload) NumActivePlayers() int {
return p.numActivePlayers
}
func (p PlayersSyncedEventPayload) NumInactivePlayers() int {
return p.numPlayers - p.numActivePlayers
}
type VillagesSyncedEventPayload struct { type VillagesSyncedEventPayload struct {
serverKey string serverKey string
serverURL *url.URL serverURL *url.URL

View File

@ -59,18 +59,22 @@ func TestNewTribesSyncedEventPayload(t *testing.T) {
server := domaintest.NewServer(t) server := domaintest.NewServer(t)
numTribes := gofakeit.IntRange(0, math.MaxInt) numTribes := gofakeit.IntRange(0, math.MaxInt)
numActiveTribes := gofakeit.IntRange(0, math.MaxInt)
payload, err := domain.NewTribesSyncedEventPayload( payload, err := domain.NewTribesSyncedEventPayload(
server.Key(), server.Key(),
server.URL(), server.URL(),
server.VersionCode(), server.VersionCode(),
numTribes, numTribes,
numActiveTribes,
) )
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, server.Key(), payload.ServerKey()) assert.Equal(t, server.Key(), payload.ServerKey())
assert.Equal(t, server.URL(), payload.ServerURL()) assert.Equal(t, server.URL(), payload.ServerURL())
assert.Equal(t, server.VersionCode(), payload.VersionCode()) assert.Equal(t, server.VersionCode(), payload.VersionCode())
assert.Equal(t, numTribes, payload.NumTribes()) assert.Equal(t, numTribes, payload.NumTribes())
assert.Equal(t, numActiveTribes, payload.NumActiveTribes())
assert.Equal(t, numTribes-numActiveTribes, payload.NumInactiveTribes())
} }
func TestNewPlayersSyncedEventPayload(t *testing.T) { func TestNewPlayersSyncedEventPayload(t *testing.T) {
@ -78,18 +82,22 @@ func TestNewPlayersSyncedEventPayload(t *testing.T) {
server := domaintest.NewServer(t) server := domaintest.NewServer(t)
numPlayers := gofakeit.IntRange(0, math.MaxInt) numPlayers := gofakeit.IntRange(0, math.MaxInt)
numActivePlayers := gofakeit.IntRange(0, math.MaxInt)
payload, err := domain.NewPlayersSyncedEventPayload( payload, err := domain.NewPlayersSyncedEventPayload(
server.Key(), server.Key(),
server.URL(), server.URL(),
server.VersionCode(), server.VersionCode(),
numPlayers, numPlayers,
numActivePlayers,
) )
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, server.Key(), payload.ServerKey()) assert.Equal(t, server.Key(), payload.ServerKey())
assert.Equal(t, server.URL(), payload.ServerURL()) assert.Equal(t, server.URL(), payload.ServerURL())
assert.Equal(t, server.VersionCode(), payload.VersionCode()) assert.Equal(t, server.VersionCode(), payload.VersionCode())
assert.Equal(t, numPlayers, payload.NumPlayers()) assert.Equal(t, numPlayers, payload.NumPlayers())
assert.Equal(t, numActivePlayers, payload.NumActivePlayers())
assert.Equal(t, numPlayers-numActivePlayers, payload.NumInactivePlayers())
} }
func TestNewVillagesSyncedEventPayload(t *testing.T) { func TestNewVillagesSyncedEventPayload(t *testing.T) {

View File

@ -1180,6 +1180,37 @@ func (res ListPlayersWithRelationsResult) Next() PlayerCursor {
return res.next return res.next
} }
type CountPlayersParams struct {
serverKeys []string
}
const countPlayersParamsModelName = "CountPlayersParams"
func NewCountPlayersParams() CountPlayersParams {
return CountPlayersParams{}
}
func (params *CountPlayersParams) ServerKeys() []string {
return params.serverKeys
}
func (params *CountPlayersParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: countPlayersParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
type PlayerNotFoundError struct { type PlayerNotFoundError struct {
ID int ID int
ServerKey string ServerKey string

View File

@ -1614,3 +1614,57 @@ func TestNewListPlayersWithRelationsResult(t *testing.T) {
assert.True(t, res.Next().IsZero()) assert.True(t, res.Next().IsZero())
}) })
} }
func TestCountPlayersParams_SetServerKeys(t *testing.T) {
t.Parallel()
type args struct {
serverKeys []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
serverKeys: []string{
domaintest.RandServerKey(),
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
serverKeys: []string{serverKeyTest.key},
},
expectedErr: domain.SliceElementValidationError{
Model: "CountPlayersParams",
Field: "serverKeys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewCountPlayersParams()
require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
})
}
}

View File

@ -15,7 +15,11 @@ type Server struct {
open bool open bool
special bool special bool
numPlayers int numPlayers int
numActivePlayers int
numInactivePlayers int
numTribes int numTribes int
numActiveTribes int
numInactiveTribes int
numVillages int numVillages int
numPlayerVillages int numPlayerVillages int
numBarbarianVillages int numBarbarianVillages int
@ -24,6 +28,7 @@ type Server struct {
buildingInfo BuildingInfo buildingInfo BuildingInfo
unitInfo UnitInfo unitInfo UnitInfo
createdAt time.Time createdAt time.Time
snapshotCreatedAt time.Time
playerDataSyncedAt time.Time playerDataSyncedAt time.Time
playerSnapshotsCreatedAt time.Time playerSnapshotsCreatedAt time.Time
tribeDataSyncedAt time.Time tribeDataSyncedAt time.Time
@ -45,7 +50,11 @@ func UnmarshalServerFromDatabase(
open bool, open bool,
special bool, special bool,
numPlayers int, numPlayers int,
numActivePlayers int,
numInactivePlayers int,
numTribes int, numTribes int,
numActiveTribes int,
numInactiveTribes int,
numVillages int, numVillages int,
numPlayerVillages int, numPlayerVillages int,
numBarbarianVillages int, numBarbarianVillages int,
@ -54,6 +63,7 @@ func UnmarshalServerFromDatabase(
buildingInfo BuildingInfo, buildingInfo BuildingInfo,
unitInfo UnitInfo, unitInfo UnitInfo,
createdAt time.Time, createdAt time.Time,
snapshotCreatedAt time.Time,
playerDataSyncedAt time.Time, playerDataSyncedAt time.Time,
playerSnapshotsCreatedAt time.Time, playerSnapshotsCreatedAt time.Time,
tribeDataSyncedAt time.Time, tribeDataSyncedAt time.Time,
@ -88,11 +98,16 @@ func UnmarshalServerFromDatabase(
return Server{ return Server{
key: key, key: key,
versionCode: versionCode,
url: u, url: u,
open: open, open: open,
special: special, special: special,
numPlayers: numPlayers, numPlayers: numPlayers,
numActivePlayers: numActivePlayers,
numInactivePlayers: numInactivePlayers,
numTribes: numTribes, numTribes: numTribes,
numActiveTribes: numActiveTribes,
numInactiveTribes: numInactiveTribes,
numVillages: numVillages, numVillages: numVillages,
numPlayerVillages: numPlayerVillages, numPlayerVillages: numPlayerVillages,
numBarbarianVillages: numBarbarianVillages, numBarbarianVillages: numBarbarianVillages,
@ -101,13 +116,13 @@ func UnmarshalServerFromDatabase(
buildingInfo: buildingInfo, buildingInfo: buildingInfo,
unitInfo: unitInfo, unitInfo: unitInfo,
createdAt: createdAt, createdAt: createdAt,
snapshotCreatedAt: snapshotCreatedAt,
playerDataSyncedAt: playerDataSyncedAt, playerDataSyncedAt: playerDataSyncedAt,
playerSnapshotsCreatedAt: playerSnapshotsCreatedAt, playerSnapshotsCreatedAt: playerSnapshotsCreatedAt,
tribeDataSyncedAt: tribeDataSyncedAt, tribeDataSyncedAt: tribeDataSyncedAt,
tribeSnapshotsCreatedAt: tribeSnapshotsCreatedAt, tribeSnapshotsCreatedAt: tribeSnapshotsCreatedAt,
villageDataSyncedAt: villageDataSyncedAt, villageDataSyncedAt: villageDataSyncedAt,
ennoblementDataSyncedAt: ennoblementDataSyncedAt, ennoblementDataSyncedAt: ennoblementDataSyncedAt,
versionCode: versionCode,
}, nil }, nil
} }
@ -135,10 +150,26 @@ func (s Server) NumPlayers() int {
return s.numPlayers return s.numPlayers
} }
func (s Server) NumActivePlayers() int {
return s.numActivePlayers
}
func (s Server) NumInactivePlayers() int {
return s.numInactivePlayers
}
func (s Server) NumTribes() int { func (s Server) NumTribes() int {
return s.numTribes return s.numTribes
} }
func (s Server) NumActiveTribes() int {
return s.numActiveTribes
}
func (s Server) NumInactiveTribes() int {
return s.numInactiveTribes
}
func (s Server) NumVillages() int { func (s Server) NumVillages() int {
return s.numVillages return s.numVillages
} }
@ -171,6 +202,10 @@ func (s Server) CreatedAt() time.Time {
return s.createdAt return s.createdAt
} }
func (s Server) SnapshotCreatedAt() time.Time {
return s.snapshotCreatedAt
}
func (s Server) PlayerDataSyncedAt() time.Time { func (s Server) PlayerDataSyncedAt() time.Time {
return s.playerDataSyncedAt return s.playerDataSyncedAt
} }
@ -378,8 +413,12 @@ type UpdateServerParams struct {
buildingInfo NullBuildingInfo buildingInfo NullBuildingInfo
unitInfo NullUnitInfo unitInfo NullUnitInfo
numTribes NullInt numTribes NullInt
numActiveTribes NullInt
numInactiveTribes NullInt
tribeDataSyncedAt NullTime tribeDataSyncedAt NullTime
numPlayers NullInt numPlayers NullInt
numActivePlayers NullInt
numInactivePlayers NullInt
playerDataSyncedAt NullTime playerDataSyncedAt NullTime
numVillages NullInt numVillages NullInt
numPlayerVillages NullInt numPlayerVillages NullInt
@ -387,6 +426,7 @@ type UpdateServerParams struct {
numBonusVillages NullInt numBonusVillages NullInt
villageDataSyncedAt NullTime villageDataSyncedAt NullTime
ennoblementDataSyncedAt NullTime ennoblementDataSyncedAt NullTime
snapshotCreatedAt NullTime
tribeSnapshotsCreatedAt NullTime tribeSnapshotsCreatedAt NullTime
playerSnapshotsCreatedAt NullTime playerSnapshotsCreatedAt NullTime
} }
@ -424,9 +464,9 @@ func (params *UpdateServerParams) NumTribes() NullInt {
return params.numTribes return params.numTribes
} }
func (params *UpdateServerParams) SetNumTribes(numTribes NullInt) error { func (params *UpdateServerParams) SetNumTribes(num NullInt) error {
if numTribes.Valid { if num.Valid {
if err := validateIntInRange(numTribes.V, 0, math.MaxInt); err != nil { if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{ return ValidationError{
Model: updateServerParamsModelName, Model: updateServerParamsModelName,
Field: "numTribes", Field: "numTribes",
@ -435,7 +475,47 @@ func (params *UpdateServerParams) SetNumTribes(numTribes NullInt) error {
} }
} }
params.numTribes = numTribes params.numTribes = num
return nil
}
func (params *UpdateServerParams) NumActiveTribes() NullInt {
return params.numActiveTribes
}
func (params *UpdateServerParams) SetNumActiveTribes(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numActiveTribes",
Err: err,
}
}
}
params.numActiveTribes = num
return nil
}
func (params *UpdateServerParams) NumInactiveTribes() NullInt {
return params.numInactiveTribes
}
func (params *UpdateServerParams) SetNumInactiveTribes(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numInactiveTribes",
Err: err,
}
}
}
params.numInactiveTribes = num
return nil return nil
} }
@ -453,9 +533,9 @@ func (params *UpdateServerParams) NumPlayers() NullInt {
return params.numPlayers return params.numPlayers
} }
func (params *UpdateServerParams) SetNumPlayers(numPlayers NullInt) error { func (params *UpdateServerParams) SetNumPlayers(num NullInt) error {
if numPlayers.Valid { if num.Valid {
if err := validateIntInRange(numPlayers.V, 0, math.MaxInt); err != nil { if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{ return ValidationError{
Model: updateServerParamsModelName, Model: updateServerParamsModelName,
Field: "numPlayers", Field: "numPlayers",
@ -464,7 +544,47 @@ func (params *UpdateServerParams) SetNumPlayers(numPlayers NullInt) error {
} }
} }
params.numPlayers = numPlayers params.numPlayers = num
return nil
}
func (params *UpdateServerParams) NumActivePlayers() NullInt {
return params.numActivePlayers
}
func (params *UpdateServerParams) SetNumActivePlayers(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numActivePlayers",
Err: err,
}
}
}
params.numActivePlayers = num
return nil
}
func (params *UpdateServerParams) NumInactivePlayers() NullInt {
return params.numInactivePlayers
}
func (params *UpdateServerParams) SetNumInactivePlayers(num NullInt) error {
if num.Valid {
if err := validateIntInRange(num.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numInactivePlayers",
Err: err,
}
}
}
params.numInactivePlayers = num
return nil return nil
} }
@ -576,6 +696,15 @@ func (params *UpdateServerParams) SetEnnoblementDataSyncedAt(ennoblementDataSync
return nil return nil
} }
func (params *UpdateServerParams) SnapshotCreatedAt() NullTime {
return params.snapshotCreatedAt
}
func (params *UpdateServerParams) SetSnapshotCreatedAt(snapshotCreatedAt NullTime) error {
params.snapshotCreatedAt = snapshotCreatedAt
return nil
}
func (params *UpdateServerParams) TribeSnapshotsCreatedAt() NullTime { func (params *UpdateServerParams) TribeSnapshotsCreatedAt() NullTime {
return params.tribeSnapshotsCreatedAt return params.tribeSnapshotsCreatedAt
} }
@ -599,9 +728,9 @@ func (params *UpdateServerParams) IsZero() bool {
return !params.config.Valid && return !params.config.Valid &&
!params.buildingInfo.Valid && !params.buildingInfo.Valid &&
!params.unitInfo.Valid && !params.unitInfo.Valid &&
!params.numTribes.Valid && !params.numActiveTribes.Valid &&
!params.tribeDataSyncedAt.Valid && !params.tribeDataSyncedAt.Valid &&
!params.numPlayers.Valid && !params.numActivePlayers.Valid &&
!params.playerDataSyncedAt.Valid && !params.playerDataSyncedAt.Valid &&
!params.numVillages.Valid && !params.numVillages.Valid &&
!params.numPlayerVillages.Valid && !params.numPlayerVillages.Valid &&
@ -609,6 +738,7 @@ func (params *UpdateServerParams) IsZero() bool {
!params.numBonusVillages.Valid && !params.numBonusVillages.Valid &&
!params.villageDataSyncedAt.Valid && !params.villageDataSyncedAt.Valid &&
!params.ennoblementDataSyncedAt.Valid && !params.ennoblementDataSyncedAt.Valid &&
!params.snapshotCreatedAt.Valid &&
!params.tribeSnapshotsCreatedAt.Valid && !params.tribeSnapshotsCreatedAt.Valid &&
!params.playerSnapshotsCreatedAt.Valid !params.playerSnapshotsCreatedAt.Valid
} }
@ -713,6 +843,7 @@ type ListServersParams struct {
versionCodes []string versionCodes []string
open NullBool open NullBool
special NullBool special NullBool
snapshotCreatedAtLT NullTime
tribeSnapshotsCreatedAtLT NullTime tribeSnapshotsCreatedAtLT NullTime
playerSnapshotsCreatedAtLT NullTime playerSnapshotsCreatedAtLT NullTime
sort []ServerSort sort []ServerSort
@ -796,6 +927,15 @@ func (params *ListServersParams) SetSpecial(special NullBool) error {
return nil return nil
} }
func (params *ListServersParams) SnapshotCreatedAtLT() NullTime {
return params.snapshotCreatedAtLT
}
func (params *ListServersParams) SetSnapshotCreatedAtLT(snapshotCreatedAtLT NullTime) error {
params.snapshotCreatedAtLT = snapshotCreatedAtLT
return nil
}
func (params *ListServersParams) TribeSnapshotsCreatedAtLT() NullTime { func (params *ListServersParams) TribeSnapshotsCreatedAtLT() NullTime {
return params.tribeSnapshotsCreatedAtLT return params.tribeSnapshotsCreatedAtLT
} }

View File

@ -0,0 +1,658 @@
package domain
import (
"errors"
"math"
"time"
)
type ServerSnapshot struct {
id int
serverKey string
numPlayers int
numActivePlayers int
numInactivePlayers int
numTribes int
numActiveTribes int
numInactiveTribes int
numVillages int
numPlayerVillages int
numBarbarianVillages int
numBonusVillages int
date time.Time
createdAt time.Time
}
const serverSnapshotModelName = "ServerSnapshot"
// UnmarshalServerSnapshotFromDatabase unmarshals ServerSnapshot from the database.
//
// It should be used only for unmarshalling from the database!
// You can't use UnmarshalServerSnapshotFromDatabase as constructor - It may put domain into the invalid state!
func UnmarshalServerSnapshotFromDatabase(
id int,
serverKey string,
numPlayers int,
numActivePlayers int,
numInactivePlayers int,
numTribes int,
numActiveTribes int,
numInactiveTribes int,
numVillages int,
numPlayerVillages int,
numBarbarianVillages int,
numBonusVillages int,
date time.Time,
createdAt time.Time,
) (ServerSnapshot, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return ServerSnapshot{}, ValidationError{
Model: serverSnapshotModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return ServerSnapshot{}, ValidationError{
Model: serverSnapshotModelName,
Field: "serverKey",
Err: err,
}
}
return ServerSnapshot{
id: id,
serverKey: serverKey,
numPlayers: numPlayers,
numActivePlayers: numActivePlayers,
numInactivePlayers: numInactivePlayers,
numTribes: numTribes,
numActiveTribes: numActiveTribes,
numInactiveTribes: numInactiveTribes,
numVillages: numVillages,
numPlayerVillages: numPlayerVillages,
numBarbarianVillages: numBarbarianVillages,
numBonusVillages: numBonusVillages,
date: date,
createdAt: createdAt,
}, nil
}
func (ss ServerSnapshot) ID() int {
return ss.id
}
func (ss ServerSnapshot) ServerKey() string {
return ss.serverKey
}
func (ss ServerSnapshot) NumPlayers() int {
return ss.numPlayers
}
func (ss ServerSnapshot) NumActivePlayers() int {
return ss.numActivePlayers
}
func (ss ServerSnapshot) NumInactivePlayers() int {
return ss.numInactivePlayers
}
func (ss ServerSnapshot) NumTribes() int {
return ss.numTribes
}
func (ss ServerSnapshot) NumActiveTribes() int {
return ss.numActiveTribes
}
func (ss ServerSnapshot) NumInactiveTribes() int {
return ss.numInactiveTribes
}
func (ss ServerSnapshot) NumVillages() int {
return ss.numVillages
}
func (ss ServerSnapshot) NumPlayerVillages() int {
return ss.numPlayerVillages
}
func (ss ServerSnapshot) NumBarbarianVillages() int {
return ss.numBarbarianVillages
}
func (ss ServerSnapshot) NumBonusVillages() int {
return ss.numBonusVillages
}
func (ss ServerSnapshot) Date() time.Time {
return ss.date
}
func (ss ServerSnapshot) CreatedAt() time.Time {
return ss.createdAt
}
func (ss ServerSnapshot) WithRelations(server ServerMeta) ServerSnapshotWithRelations {
return ServerSnapshotWithRelations{
snapshot: ss,
server: server,
}
}
func (ss ServerSnapshot) ToCursor() (ServerSnapshotCursor, error) {
return NewServerSnapshotCursor(ss.id, ss.serverKey, ss.date)
}
func (ss ServerSnapshot) IsZero() bool {
return ss == ServerSnapshot{}
}
type ServerSnapshots []ServerSnapshot
type ServerSnapshotWithRelations struct {
snapshot ServerSnapshot
server ServerMeta
}
func (ts ServerSnapshotWithRelations) ServerSnapshot() ServerSnapshot {
return ts.snapshot
}
func (ts ServerSnapshotWithRelations) Server() ServerMeta {
return ts.server
}
func (ts ServerSnapshotWithRelations) IsZero() bool {
return ts.snapshot.IsZero()
}
type ServerSnapshotsWithRelations []ServerSnapshotWithRelations
type CreateServerSnapshotParams struct {
serverKey string
numPlayers int
numActivePlayers int
numInactivePlayers int
numTribes int
numActiveTribes int
numInactiveTribes int
numVillages int
numPlayerVillages int
numBarbarianVillages int
numBonusVillages int
date time.Time
}
func NewCreateServerSnapshotParams(server Server, date time.Time) (CreateServerSnapshotParams, error) {
if server.IsZero() {
return CreateServerSnapshotParams{}, errors.New("given server is an empty struct")
}
if !server.Open() {
return CreateServerSnapshotParams{}, errors.New("given server is closed")
}
return CreateServerSnapshotParams{
serverKey: server.Key(),
numPlayers: server.NumPlayers(),
numActivePlayers: server.NumActivePlayers(),
numInactivePlayers: server.NumInactivePlayers(),
numTribes: server.NumTribes(),
numActiveTribes: server.NumActiveTribes(),
numInactiveTribes: server.NumInactiveTribes(),
numVillages: server.NumVillages(),
numPlayerVillages: server.NumPlayerVillages(),
numBarbarianVillages: server.NumBarbarianVillages(),
numBonusVillages: server.NumBonusVillages(),
date: date,
}, nil
}
func (params CreateServerSnapshotParams) ServerKey() string {
return params.serverKey
}
func (params CreateServerSnapshotParams) NumPlayers() int {
return params.numPlayers
}
func (params CreateServerSnapshotParams) NumActivePlayers() int {
return params.numActivePlayers
}
func (params CreateServerSnapshotParams) NumInactivePlayers() int {
return params.numInactivePlayers
}
func (params CreateServerSnapshotParams) NumTribes() int {
return params.numTribes
}
func (params CreateServerSnapshotParams) NumActiveTribes() int {
return params.numActiveTribes
}
func (params CreateServerSnapshotParams) NumInactiveTribes() int {
return params.numInactiveTribes
}
func (params CreateServerSnapshotParams) NumVillages() int {
return params.numVillages
}
func (params CreateServerSnapshotParams) NumPlayerVillages() int {
return params.numPlayerVillages
}
func (params CreateServerSnapshotParams) NumBarbarianVillages() int {
return params.numBarbarianVillages
}
func (params CreateServerSnapshotParams) NumBonusVillages() int {
return params.numBonusVillages
}
func (params CreateServerSnapshotParams) Date() time.Time {
return params.date
}
type ServerSnapshotSort uint8
const (
ServerSnapshotSortDateASC ServerSnapshotSort = iota + 1
ServerSnapshotSortDateDESC
ServerSnapshotSortIDASC
ServerSnapshotSortIDDESC
ServerSnapshotSortServerKeyASC
ServerSnapshotSortServerKeyDESC
)
// IsInConflict returns true if two sorts can't be used together
// (e.g. ServerSnapshotSortIDASC and ServerSnapshotSortIDDESC).
func (s ServerSnapshotSort) IsInConflict(s2 ServerSnapshotSort) bool {
return isSortInConflict(s, s2)
}
//nolint:gocyclo
func (s ServerSnapshotSort) String() string {
switch s {
case ServerSnapshotSortDateASC:
return "date:ASC"
case ServerSnapshotSortDateDESC:
return "date:DESC"
case ServerSnapshotSortIDASC:
return "id:ASC"
case ServerSnapshotSortIDDESC:
return "id:DESC"
case ServerSnapshotSortServerKeyASC:
return "serverKey:ASC"
case ServerSnapshotSortServerKeyDESC:
return "serverKey:DESC"
default:
return "unknown server snapshot sort"
}
}
type ServerSnapshotCursor struct {
id int
serverKey string
date time.Time
}
const serverSnapshotCursorModelName = "ServerSnapshotCursor"
func NewServerSnapshotCursor(id int, serverKey string, date time.Time) (ServerSnapshotCursor, error) {
if err := validateIntInRange(id, 1, math.MaxInt); err != nil {
return ServerSnapshotCursor{}, ValidationError{
Model: serverSnapshotCursorModelName,
Field: "id",
Err: err,
}
}
if err := validateServerKey(serverKey); err != nil {
return ServerSnapshotCursor{}, ValidationError{
Model: serverSnapshotCursorModelName,
Field: "serverKey",
Err: err,
}
}
return ServerSnapshotCursor{
id: id,
serverKey: serverKey,
date: date,
}, nil
}
func decodeServerSnapshotCursor(encoded string) (ServerSnapshotCursor, error) {
m, err := decodeCursor(encoded)
if err != nil {
return ServerSnapshotCursor{}, err
}
id, err := m.int("id")
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
serverKey, err := m.string("serverKey")
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
date, err := m.time("date")
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
tsc, err := NewServerSnapshotCursor(
id,
serverKey,
date,
)
if err != nil {
return ServerSnapshotCursor{}, ErrInvalidCursor
}
return tsc, nil
}
func (ssc ServerSnapshotCursor) ID() int {
return ssc.id
}
func (ssc ServerSnapshotCursor) ServerKey() string {
return ssc.serverKey
}
func (ssc ServerSnapshotCursor) Date() time.Time {
return ssc.date
}
func (ssc ServerSnapshotCursor) IsZero() bool {
return ssc == ServerSnapshotCursor{}
}
func (ssc ServerSnapshotCursor) Encode() string {
if ssc.IsZero() {
return ""
}
return encodeCursor([]keyValuePair{
{"id", ssc.id},
{"serverKey", ssc.serverKey},
{"date", ssc.date},
})
}
type ListServerSnapshotsParams struct {
serverKeys []string
sort []ServerSnapshotSort
cursor ServerSnapshotCursor
limit int
}
const (
ServerSnapshotListMaxLimit = 500
listServerSnapshotsParamsModelName = "ListServerSnapshotsParams"
)
func NewListServerSnapshotsParams() ListServerSnapshotsParams {
return ListServerSnapshotsParams{
sort: []ServerSnapshotSort{
ServerSnapshotSortServerKeyASC,
ServerSnapshotSortDateASC,
ServerSnapshotSortIDASC,
},
limit: ServerSnapshotListMaxLimit,
}
}
func (params *ListServerSnapshotsParams) ServerKeys() []string {
return params.serverKeys
}
func (params *ListServerSnapshotsParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
func (params *ListServerSnapshotsParams) Sort() []ServerSnapshotSort {
return params.sort
}
const (
serverSnapshotSortMinLength = 0
serverSnapshotSortMaxLength = 3
)
func (params *ListServerSnapshotsParams) SetSort(sort []ServerSnapshotSort) error {
if err := validateSort(sort, serverSnapshotSortMinLength, serverSnapshotSortMaxLength); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Err: err,
}
}
params.sort = sort
return nil
}
func (params *ListServerSnapshotsParams) PrependSort(sort []ServerSnapshotSort) error {
if len(sort) == 0 {
return nil
}
if err := validateSliceLen(sort, 0, max(serverSnapshotSortMaxLength-len(params.sort), 0)); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Err: err,
}
}
return params.SetSort(append(sort, params.sort...))
}
func (params *ListServerSnapshotsParams) PrependSortString(
sort []string,
allowed []ServerSnapshotSort,
maxLength int,
) error {
if len(sort) == 0 {
return nil
}
if err := validateSliceLen(sort, 0, max(min(serverSnapshotSortMaxLength-len(params.sort), maxLength), 0)); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Err: err,
}
}
toPrepend := make([]ServerSnapshotSort, 0, len(sort))
for i, s := range sort {
converted, err := newSortFromString(s, allowed...)
if err != nil {
return SliceElementValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "sort",
Index: i,
Err: err,
}
}
toPrepend = append(toPrepend, converted)
}
return params.SetSort(append(toPrepend, params.sort...))
}
func (params *ListServerSnapshotsParams) Cursor() ServerSnapshotCursor {
return params.cursor
}
func (params *ListServerSnapshotsParams) SetCursor(cursor ServerSnapshotCursor) error {
params.cursor = cursor
return nil
}
func (params *ListServerSnapshotsParams) SetEncodedCursor(encoded string) error {
decoded, err := decodeServerSnapshotCursor(encoded)
if err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "cursor",
Err: err,
}
}
params.cursor = decoded
return nil
}
func (params *ListServerSnapshotsParams) Limit() int {
return params.limit
}
func (params *ListServerSnapshotsParams) SetLimit(limit int) error {
if err := validateIntInRange(limit, 1, ServerSnapshotListMaxLimit); err != nil {
return ValidationError{
Model: listServerSnapshotsParamsModelName,
Field: "limit",
Err: err,
}
}
params.limit = limit
return nil
}
type ListServerSnapshotsResult struct {
snapshots ServerSnapshots
self ServerSnapshotCursor
next ServerSnapshotCursor
}
const listServerSnapshotsResultModelName = "ListServerSnapshotsResult"
func NewListServerSnapshotsResult(
snapshots ServerSnapshots,
next ServerSnapshot,
) (ListServerSnapshotsResult, error) {
var err error
res := ListServerSnapshotsResult{
snapshots: snapshots,
}
if len(snapshots) > 0 {
res.self, err = snapshots[0].ToCursor()
if err != nil {
return ListServerSnapshotsResult{}, ValidationError{
Model: listServerSnapshotsResultModelName,
Field: "self",
Err: err,
}
}
}
if !next.IsZero() {
res.next, err = next.ToCursor()
if err != nil {
return ListServerSnapshotsResult{}, ValidationError{
Model: listServerSnapshotsResultModelName,
Field: "next",
Err: err,
}
}
}
return res, nil
}
func (res ListServerSnapshotsResult) ServerSnapshots() ServerSnapshots {
return res.snapshots
}
func (res ListServerSnapshotsResult) Self() ServerSnapshotCursor {
return res.self
}
func (res ListServerSnapshotsResult) Next() ServerSnapshotCursor {
return res.next
}
type ListServerSnapshotsWithRelationsResult struct {
snapshots ServerSnapshotsWithRelations
self ServerSnapshotCursor
next ServerSnapshotCursor
}
const listServerSnapshotsWithRelationsResultModelName = "ListServerSnapshotsWithRelationsResult"
func NewListServerSnapshotsWithRelationsResult(
snapshots ServerSnapshotsWithRelations,
next ServerSnapshotWithRelations,
) (ListServerSnapshotsWithRelationsResult, error) {
var err error
res := ListServerSnapshotsWithRelationsResult{
snapshots: snapshots,
}
if len(snapshots) > 0 {
res.self, err = snapshots[0].ServerSnapshot().ToCursor()
if err != nil {
return ListServerSnapshotsWithRelationsResult{}, ValidationError{
Model: listServerSnapshotsWithRelationsResultModelName,
Field: "self",
Err: err,
}
}
}
if !next.IsZero() {
res.next, err = next.ServerSnapshot().ToCursor()
if err != nil {
return ListServerSnapshotsWithRelationsResult{}, ValidationError{
Model: listServerSnapshotsWithRelationsResultModelName,
Field: "next",
Err: err,
}
}
}
return res, nil
}
func (res ListServerSnapshotsWithRelationsResult) ServerSnapshots() ServerSnapshotsWithRelations {
return res.snapshots
}
func (res ListServerSnapshotsWithRelationsResult) Self() ServerSnapshotCursor {
return res.self
}
func (res ListServerSnapshotsWithRelationsResult) Next() ServerSnapshotCursor {
return res.next
}

View File

@ -0,0 +1,901 @@
package domain_test
import (
"fmt"
"testing"
"time"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/domain/domaintest"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCreateServerSnapshotParams(t *testing.T) {
t.Parallel()
server := domaintest.NewServer(t)
date := time.Now()
params, err := domain.NewCreateServerSnapshotParams(server, date)
require.NoError(t, err)
assert.Equal(t, server.Key(), params.ServerKey())
assert.Equal(t, server.NumPlayers(), params.NumPlayers())
assert.Equal(t, server.NumActivePlayers(), params.NumActivePlayers())
assert.Equal(t, server.NumInactivePlayers(), params.NumInactivePlayers())
assert.Equal(t, server.NumTribes(), params.NumTribes())
assert.Equal(t, server.NumActiveTribes(), params.NumActiveTribes())
assert.Equal(t, server.NumInactiveTribes(), params.NumInactiveTribes())
assert.Equal(t, server.NumVillages(), params.NumVillages())
assert.Equal(t, server.NumPlayerVillages(), params.NumPlayerVillages())
assert.Equal(t, server.NumBarbarianVillages(), params.NumBarbarianVillages())
assert.Equal(t, server.NumBonusVillages(), params.NumBonusVillages())
assert.Equal(t, date, params.Date())
}
func TestServerSnapshotSort_IsInConflict(t *testing.T) {
t.Parallel()
type args struct {
sorts [2]domain.ServerSnapshotSort
}
tests := []struct {
name string
args args
expectedRes bool
}{
{
name: "OK: id:ASC serverKey:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC, domain.ServerSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:DESC serverKey:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDDESC, domain.ServerSnapshotSortServerKeyASC},
},
expectedRes: false,
},
{
name: "OK: id:ASC id:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC, domain.ServerSnapshotSortIDASC},
},
expectedRes: true,
},
{
name: "OK: id:ASC id:DESC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC, domain.ServerSnapshotSortIDDESC},
},
expectedRes: true,
},
{
name: "OK: date:ASC date:DESC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortDateASC, domain.ServerSnapshotSortDateDESC},
},
expectedRes: true,
},
{
name: "OK: serverKey:DESC serverKey:ASC",
args: args{
sorts: [2]domain.ServerSnapshotSort{domain.ServerSnapshotSortServerKeyDESC, domain.ServerSnapshotSortServerKeyASC},
},
expectedRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectedRes, tt.args.sorts[0].IsInConflict(tt.args.sorts[1]))
})
}
}
func TestNewServerSnapshotCursor(t *testing.T) {
t.Parallel()
validServerSnapshotCursor := domaintest.NewServerSnapshotCursor(t)
type args struct {
id int
serverKey string
date time.Time
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
id: validServerSnapshotCursor.ID(),
serverKey: validServerSnapshotCursor.ServerKey(),
date: validServerSnapshotCursor.Date(),
},
expectedErr: nil,
},
{
name: "ERR: id < 1",
args: args{
id: 0,
serverKey: validServerSnapshotCursor.ServerKey(),
date: validServerSnapshotCursor.Date(),
},
expectedErr: domain.ValidationError{
Model: "ServerSnapshotCursor",
Field: "id",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
id: validServerSnapshotCursor.ID(),
serverKey: serverKeyTest.key,
},
expectedErr: domain.ValidationError{
Model: "ServerSnapshotCursor",
Field: "serverKey",
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ssc, err := domain.NewServerSnapshotCursor(
tt.args.id,
tt.args.serverKey,
tt.args.date,
)
require.ErrorIs(t, err, tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.id, ssc.ID())
assert.Equal(t, tt.args.serverKey, ssc.ServerKey())
assert.Equal(t, tt.args.date, ssc.Date())
assert.NotEmpty(t, ssc.Encode())
})
}
}
func TestListServerSnapshotsParams_SetServerKeys(t *testing.T) {
t.Parallel()
type args struct {
serverKeys []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
serverKeys: []string{
domaintest.RandServerKey(),
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
serverKeys: []string{serverKeyTest.key},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListServerSnapshotsParams",
Field: "serverKeys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
})
}
}
func TestListServerSnapshotsParams_SetSort(t *testing.T) {
t.Parallel()
type args struct {
sort []domain.ServerSnapshotSort
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortServerKeyASC,
},
},
},
{
name: "OK: empty slice",
args: args{
sort: nil,
},
},
{
name: "ERR: len(sort) > 3",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 3,
Current: 4,
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.ServerSnapshotSortIDASC.String(), domain.ServerSnapshotSortIDDESC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
require.ErrorIs(t, params.SetSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.sort, params.Sort())
})
}
}
func TestListServerSnapshotsParams_PrependSort(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
return domain.ListServerSnapshotsParams{}
}
type args struct {
sort []domain.ServerSnapshotSort
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListServerSnapshotsParams
args args
expectedErr error
}{
{
name: "OK",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortDateASC,
},
},
},
{
name: "OK: custom params",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
},
},
},
{
name: "OK: empty slice",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: nil,
},
},
{
name: "ERR: custom params + len(sort) > sortMaxLength - len(sort)",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: len(sort) > 3",
newParams: defaultNewParams,
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateASC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 3,
Current: 4,
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC,
},
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.ServerSnapshotSortDateASC.String(), domain.ServerSnapshotSortDateDESC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newParams := defaultNewParams
if tt.newParams != nil {
newParams = tt.newParams
}
params := newParams(t)
expectedSort := params.Sort()
require.ErrorIs(t, params.PrependSort(tt.args.sort), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, append(tt.args.sort, expectedSort...), params.Sort())
})
}
}
func TestListServerSnapshotsParams_PrependSortString(t *testing.T) {
t.Parallel()
defaultNewParams := func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
return domain.ListServerSnapshotsParams{}
}
defaultAllowed := []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC,
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortIDDESC,
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortServerKeyDESC,
}
defaultMaxLength := 3
type args struct {
sort []string
allowed []domain.ServerSnapshotSort
maxLength int
}
tests := []struct {
name string
newParams func(t *testing.T) domain.ListServerSnapshotsParams
args args
expectedSort []domain.ServerSnapshotSort
expectedErr error
}{
{
name: "OK: [id:ASC, date:ASC, serverKey:ASC]",
args: args{
sort: []string{
"id:ASC",
"date:ASC",
"serverKey:ASC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedSort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortServerKeyASC,
},
},
{
name: "OK: [id:DESC, date:DESC, serverKey:DESC]",
args: args{
sort: []string{
"id:DESC",
"date:DESC",
"serverKey:DESC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedSort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDDESC,
domain.ServerSnapshotSortDateDESC,
domain.ServerSnapshotSortServerKeyDESC,
},
},
{
name: "OK: custom params",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
}))
return params
},
args: args{
sort: []string{
"date:ASC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedSort: []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortIDASC,
domain.ServerSnapshotSortServerKeyASC,
},
},
{
name: "OK: empty slice",
args: args{
sort: nil,
},
},
{
name: "ERR: custom params + len(sort) > sortMaxLength - len(sort)",
newParams: func(t *testing.T) domain.ListServerSnapshotsParams {
t.Helper()
params := domain.NewListServerSnapshotsParams()
require.NoError(t, params.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortIDASC,
}))
return params
},
args: args{
sort: []string{
"date:ASC",
"date:DESC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: len(sort) > maxLength",
newParams: defaultNewParams,
args: args{
sort: []string{
"serverKey:ASC",
"date:ASC",
},
allowed: defaultAllowed,
maxLength: 1,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: 2,
},
},
},
{
name: "ERR: unsupported sort string",
newParams: defaultNewParams,
args: args{
sort: []string{
"date:",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedErr: domain.SliceElementValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Index: 0,
Err: domain.UnsupportedSortStringError{
Sort: "date:",
},
},
},
{
name: "ERR: conflict",
args: args{
sort: []string{
"date:ASC",
"date:DESC",
},
allowed: defaultAllowed,
maxLength: defaultMaxLength,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "sort",
Err: domain.SortConflictError{
Sort: [2]string{domain.ServerSnapshotSortDateASC.String(), domain.ServerSnapshotSortDateDESC.String()},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
newParams := defaultNewParams
if tt.newParams != nil {
newParams = tt.newParams
}
params := newParams(t)
require.ErrorIs(t, params.PrependSortString(tt.args.sort, tt.args.allowed, tt.args.maxLength), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.expectedSort, params.Sort())
})
}
}
func TestListServerSnapshotsParams_SetEncodedCursor(t *testing.T) {
t.Parallel()
validCursor := domaintest.NewServerSnapshotCursor(t)
type args struct {
cursor string
}
tests := []struct {
name string
args args
expectedCursor domain.ServerSnapshotCursor
expectedErr error
}{
{
name: "OK",
args: args{
cursor: validCursor.Encode(),
},
expectedCursor: validCursor,
},
{
name: "ERR: len(cursor) < 1",
args: args{
cursor: "",
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
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: "ListServerSnapshotsParams",
Field: "cursor",
Err: domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: 1001,
},
},
},
{
name: "ERR: malformed base64",
args: args{
cursor: "112345",
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "cursor",
Err: domain.ErrInvalidCursor,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
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 TestListServerSnapshotsParams_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.ServerSnapshotListMaxLimit,
},
},
{
name: "ERR: limit < 1",
args: args{
limit: 0,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "limit",
Err: domain.MinGreaterEqualError{
Min: 1,
Current: 0,
},
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.ServerSnapshotListMaxLimit),
args: args{
limit: domain.ServerSnapshotListMaxLimit + 1,
},
expectedErr: domain.ValidationError{
Model: "ListServerSnapshotsParams",
Field: "limit",
Err: domain.MaxLessEqualError{
Max: domain.ServerSnapshotListMaxLimit,
Current: domain.ServerSnapshotListMaxLimit + 1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServerSnapshotsParams()
require.ErrorIs(t, params.SetLimit(tt.args.limit), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.limit, params.Limit())
})
}
}
func TestNewListServerSnapshotsResult(t *testing.T) {
t.Parallel()
snapshots := domain.ServerSnapshots{
domaintest.NewServerSnapshot(t),
domaintest.NewServerSnapshot(t),
domaintest.NewServerSnapshot(t),
}
next := domaintest.NewServerSnapshot(t)
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsResult(snapshots, next)
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].Date(), res.Self().Date())
assert.Equal(t, next.ID(), res.Next().ID())
assert.Equal(t, next.ServerKey(), res.Next().ServerKey())
assert.Equal(t, next.Date(), res.Next().Date())
})
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsResult(snapshots, domain.ServerSnapshot{})
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].Date(), res.Self().Date())
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 snapshots", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsResult(nil, domain.ServerSnapshot{})
require.NoError(t, err)
assert.Zero(t, res.ServerSnapshots())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}
func TestNewListServerSnapshotsWithRelationsResult(t *testing.T) {
t.Parallel()
snapshots := domain.ServerSnapshotsWithRelations{
domaintest.NewServerSnapshotWithRelations(t),
domaintest.NewServerSnapshotWithRelations(t),
domaintest.NewServerSnapshotWithRelations(t),
}
next := domaintest.NewServerSnapshotWithRelations(t)
t.Run("OK: with next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsWithRelationsResult(snapshots, next)
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ServerSnapshot().ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerSnapshot().ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].ServerSnapshot().Date(), res.Self().Date())
assert.Equal(t, next.ServerSnapshot().ID(), res.Next().ID())
assert.Equal(t, next.ServerSnapshot().ServerKey(), res.Next().ServerKey())
assert.Equal(t, next.ServerSnapshot().Date(), res.Next().Date())
})
t.Run("OK: without next", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsWithRelationsResult(snapshots, domain.ServerSnapshotWithRelations{})
require.NoError(t, err)
assert.Equal(t, snapshots, res.ServerSnapshots())
assert.Equal(t, snapshots[0].ServerSnapshot().ID(), res.Self().ID())
assert.Equal(t, snapshots[0].ServerSnapshot().ServerKey(), res.Self().ServerKey())
assert.Equal(t, snapshots[0].ServerSnapshot().Date(), res.Self().Date())
assert.True(t, res.Next().IsZero())
})
t.Run("OK: 0 snapshots", func(t *testing.T) {
t.Parallel()
res, err := domain.NewListServerSnapshotsWithRelationsResult(nil, domain.ServerSnapshotWithRelations{})
require.NoError(t, err)
assert.Zero(t, res.ServerSnapshots())
assert.True(t, res.Self().IsZero())
assert.True(t, res.Next().IsZero())
})
}

View File

@ -224,6 +224,132 @@ func TestUpdateServerParams_SetNumTribes(t *testing.T) {
} }
} }
func TestUpdateServerParams_SetNumActiveTribes(t *testing.T) {
t.Parallel()
type args struct {
numActiveTribes domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numActiveTribes: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numActiveTribes: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numActiveTribes < 0",
args: args{
numActiveTribes: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numActiveTribes",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumActiveTribes(tt.args.numActiveTribes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numActiveTribes, params.NumActiveTribes())
})
}
}
func TestUpdateServerParams_SetNumInactiveTribes(t *testing.T) {
t.Parallel()
type args struct {
numInactiveTribes domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numInactiveTribes: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numInactiveTribes: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numInactiveTribes < 0",
args: args{
numInactiveTribes: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numInactiveTribes",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumInactiveTribes(tt.args.numInactiveTribes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numInactiveTribes, params.NumInactiveTribes())
})
}
}
func TestUpdateServerParams_SetNumPlayers(t *testing.T) { func TestUpdateServerParams_SetNumPlayers(t *testing.T) {
t.Parallel() t.Parallel()
@ -287,6 +413,132 @@ func TestUpdateServerParams_SetNumPlayers(t *testing.T) {
} }
} }
func TestUpdateServerParams_SetNumActivePlayers(t *testing.T) {
t.Parallel()
type args struct {
numActivePlayers domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numActivePlayers: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numActivePlayers: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numActivePlayers < 0",
args: args{
numActivePlayers: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numActivePlayers",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumActivePlayers(tt.args.numActivePlayers), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numActivePlayers, params.NumActivePlayers())
})
}
}
func TestUpdateServerParams_SetNumInactivePlayers(t *testing.T) {
t.Parallel()
type args struct {
numInactivePlayers domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numInactivePlayers: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numInactivePlayers: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numInactivePlayers < 0",
args: args{
numInactivePlayers: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numInactivePlayers",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumInactivePlayers(tt.args.numInactivePlayers), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numInactivePlayers, params.NumInactivePlayers())
})
}
}
func TestUpdateServerParams_SetNumVillages(t *testing.T) { func TestUpdateServerParams_SetNumVillages(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -969,6 +969,37 @@ func (res ListTribesResult) Next() TribeCursor {
return res.next return res.next
} }
type CountTribesParams struct {
serverKeys []string
}
const countTribesParamsModelName = "CountTribesParams"
func NewCountTribesParams() CountTribesParams {
return CountTribesParams{}
}
func (params *CountTribesParams) ServerKeys() []string {
return params.serverKeys
}
func (params *CountTribesParams) SetServerKeys(serverKeys []string) error {
for i, sk := range serverKeys {
if err := validateServerKey(sk); err != nil {
return SliceElementValidationError{
Model: countTribesParamsModelName,
Field: "serverKeys",
Index: i,
Err: err,
}
}
}
params.serverKeys = serverKeys
return nil
}
type TribeNotFoundError struct { type TribeNotFoundError struct {
ID int ID int
ServerKey string ServerKey string

View File

@ -318,7 +318,6 @@ func NewTribeSnapshotCursor(id int, serverKey string, date time.Time) (TribeSnap
}, nil }, nil
} }
//nolint:gocyclo
func decodeTribeSnapshotCursor(encoded string) (TribeSnapshotCursor, error) { func decodeTribeSnapshotCursor(encoded string) (TribeSnapshotCursor, error) {
m, err := decodeCursor(encoded) m, err := decodeCursor(encoded)
if err != nil { if err != nil {

View File

@ -176,7 +176,7 @@ func TestNewTribeSnapshotCursor(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
psc, err := domain.NewTribeSnapshotCursor( tsc, err := domain.NewTribeSnapshotCursor(
tt.args.id, tt.args.id,
tt.args.serverKey, tt.args.serverKey,
tt.args.date, tt.args.date,
@ -185,10 +185,10 @@ func TestNewTribeSnapshotCursor(t *testing.T) {
if tt.expectedErr != nil { if tt.expectedErr != nil {
return return
} }
assert.Equal(t, tt.args.id, psc.ID()) assert.Equal(t, tt.args.id, tsc.ID())
assert.Equal(t, tt.args.serverKey, psc.ServerKey()) assert.Equal(t, tt.args.serverKey, tsc.ServerKey())
assert.Equal(t, tt.args.date, psc.Date()) assert.Equal(t, tt.args.date, tsc.Date())
assert.NotEmpty(t, psc.Encode()) assert.NotEmpty(t, tsc.Encode())
}) })
} }
} }

View File

@ -1205,3 +1205,57 @@ func TestNewListTribesResult(t *testing.T) {
assert.True(t, res.Next().IsZero()) assert.True(t, res.Next().IsZero())
}) })
} }
func TestCountTribesParams_SetServerKeys(t *testing.T) {
t.Parallel()
type args struct {
serverKeys []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
serverKeys: []string{
domaintest.RandServerKey(),
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
serverKeys: []string{serverKeyTest.key},
},
expectedErr: domain.SliceElementValidationError{
Model: "CountTribesParams",
Field: "serverKeys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewCountTribesParams()
require.ErrorIs(t, params.SetServerKeys(tt.args.serverKeys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.serverKeys, params.ServerKeys())
})
}
}

View File

@ -12,7 +12,7 @@ type Version struct {
timezone string timezone string
} }
var versionModelName = "Version" const versionModelName = "Version"
// UnmarshalVersionFromDatabase unmarshals Version from the database. // UnmarshalVersionFromDatabase unmarshals Version from the database.
// //

View File

@ -158,10 +158,12 @@ func TestDataSync(t *testing.T) {
ctx, ctx,
port.NewServerWatermillConsumer( port.NewServerWatermillConsumer(
serverSvc, serverSvc,
nil,
serverSub, serverSub,
nopLogger, nopLogger,
marshaler, marshaler,
serverCmdSync, serverCmdSync,
"",
serverEventSynced, serverEventSynced,
tribeEventSynced, tribeEventSynced,
playerEventSynced, playerEventSynced,
@ -249,7 +251,11 @@ func TestDataSync(t *testing.T) {
assert.Equal(collect, expected["Open"], actual.Open(), msg) assert.Equal(collect, expected["Open"], actual.Open(), msg)
assert.Equal(collect, expected["VersionCode"], actual.VersionCode(), msg) assert.Equal(collect, expected["VersionCode"], actual.VersionCode(), msg)
assert.EqualValues(collect, expected["NumPlayers"], actual.NumPlayers(), msg) assert.EqualValues(collect, expected["NumPlayers"], actual.NumPlayers(), msg)
assert.EqualValues(collect, expected["NumActivePlayers"], actual.NumActivePlayers(), msg)
assert.EqualValues(collect, expected["NumInactivePlayers"], actual.NumInactivePlayers(), msg)
assert.EqualValues(collect, expected["NumTribes"], actual.NumTribes(), msg) assert.EqualValues(collect, expected["NumTribes"], actual.NumTribes(), msg)
assert.EqualValues(collect, expected["NumActiveTribes"], actual.NumActiveTribes(), msg)
assert.EqualValues(collect, expected["NumInactiveTribes"], actual.NumInactiveTribes(), msg)
assert.EqualValues(collect, expected["NumVillages"], actual.NumVillages(), msg) assert.EqualValues(collect, expected["NumVillages"], actual.NumVillages(), msg)
assert.EqualValues(collect, expected["NumPlayerVillages"], actual.NumPlayerVillages(), msg) assert.EqualValues(collect, expected["NumPlayerVillages"], actual.NumPlayerVillages(), msg)
assert.EqualValues(collect, expected["NumBonusVillages"], actual.NumBonusVillages(), msg) assert.EqualValues(collect, expected["NumBonusVillages"], actual.NumBonusVillages(), msg)
@ -257,8 +263,7 @@ func TestDataSync(t *testing.T) {
collect, collect,
expected["NumBarbarianVillages"], expected["NumBarbarianVillages"],
actual.NumBarbarianVillages(), actual.NumBarbarianVillages(),
"Key=%s", msg,
expected["Key"],
) )
assert.WithinDuration(collect, time.Now(), actual.PlayerDataSyncedAt(), time.Minute, msg) assert.WithinDuration(collect, time.Now(), actual.PlayerDataSyncedAt(), time.Minute, msg)
assert.WithinDuration(collect, time.Now(), actual.TribeDataSyncedAt(), time.Minute, msg) assert.WithinDuration(collect, time.Now(), actual.TribeDataSyncedAt(), time.Minute, msg)
@ -267,8 +272,7 @@ func TestDataSync(t *testing.T) {
collect, collect,
string(marshalJSON(collect, expected["Config"])), string(marshalJSON(collect, expected["Config"])),
string(marshalJSON(collect, serverConfigToMap(actual.Config()))), string(marshalJSON(collect, serverConfigToMap(actual.Config()))),
"Key=%s", msg,
expected["Key"],
) )
assert.JSONEq( assert.JSONEq(
collect, collect,

View File

@ -138,6 +138,7 @@ func TestEnnoblementSync(t *testing.T) {
ctx, ctx,
port.NewServerWatermillConsumer( port.NewServerWatermillConsumer(
serverSvc, serverSvc,
nil,
serverSub, serverSub,
nopLogger, nopLogger,
marshaler, marshaler,
@ -146,6 +147,7 @@ func TestEnnoblementSync(t *testing.T) {
"", "",
"", "",
"", "",
"",
ennoblementEventSynced, ennoblementEventSynced,
"", "",
"", "",

View File

@ -70,6 +70,7 @@ func TestSnapshotCreation(t *testing.T) {
) )
// events/commands // events/commands
serverSnapshotCmdCreate := gofakeit.UUID()
tribeSnapshotCmdCreate := gofakeit.UUID() tribeSnapshotCmdCreate := gofakeit.UUID()
tribeSnapshotEventCreated := gofakeit.UUID() tribeSnapshotEventCreated := gofakeit.UUID()
playerSnapshotCmdCreate := gofakeit.UUID() playerSnapshotCmdCreate := gofakeit.UUID()
@ -80,8 +81,15 @@ func TestSnapshotCreation(t *testing.T) {
serverRepo := adapter.NewServerBunRepository(db) serverRepo := adapter.NewServerBunRepository(db)
tribeRepo := adapter.NewTribeBunRepository(db) tribeRepo := adapter.NewTribeBunRepository(db)
playerRepo := adapter.NewPlayerBunRepository(db) playerRepo := adapter.NewPlayerBunRepository(db)
serverSnapshotRepo := adapter.NewServerSnapshotBunRepository(db)
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(db) tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(db)
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(db) playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(db)
serverSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
tribePub,
marshaler,
serverSnapshotCmdCreate,
"",
)
tribeSnapshotPublisher := adapter.NewSnapshotWatermillPublisher( tribeSnapshotPublisher := adapter.NewSnapshotWatermillPublisher(
tribePub, tribePub,
marshaler, marshaler,
@ -100,19 +108,28 @@ func TestSnapshotCreation(t *testing.T) {
serverSvc := app.NewServerService(serverRepo, nil, nil) serverSvc := app.NewServerService(serverRepo, nil, nil)
tribeSvc := app.NewTribeService(tribeRepo, nil, nil) tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil) playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil)
serverSnapshotSvc := app.NewServerSnapshotService(serverSnapshotRepo, serverSvc, serverSnapshotPublisher)
tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, tribeSnapshotPublisher) tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, tribeSnapshotPublisher)
playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, playerSnapshotPublisher) playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, playerSnapshotPublisher)
snapshotSvc := app.NewSnapshotService(versionSvc, serverSvc, tribeSnapshotPublisher, playerSnapshotPublisher) snapshotSvc := app.NewSnapshotService(
versionSvc,
serverSvc,
serverSnapshotPublisher,
tribeSnapshotPublisher,
playerSnapshotPublisher,
)
watermilltest.RunRouterWithContext( watermilltest.RunRouterWithContext(
t, t,
ctx, ctx,
port.NewServerWatermillConsumer( port.NewServerWatermillConsumer(
serverSvc, serverSvc,
serverSnapshotSvc,
serverSub, serverSub,
nopLogger, nopLogger,
marshaler, marshaler,
"", "",
serverSnapshotCmdCreate,
"", "",
"", "",
"", "",
@ -155,31 +172,85 @@ func TestSnapshotCreation(t *testing.T) {
assert.EventuallyWithTf(t, func(collect *assert.CollectT) { assert.EventuallyWithTf(t, func(collect *assert.CollectT) {
require.NoError(collect, ctx.Err()) require.NoError(collect, ctx.Err())
listParams := domain.NewListServersParams() listServersParams := domain.NewListServersParams()
require.NoError(collect, listParams.SetSort([]domain.ServerSort{ require.NoError(collect, listServersParams.SetSort([]domain.ServerSort{
domain.ServerSortKeyASC, domain.ServerSortKeyASC,
})) }))
require.NoError(collect, listParams.SetSpecial(domain.NullBool{ require.NoError(collect, listServersParams.SetSpecial(domain.NullBool{
V: false, V: false,
Valid: true, Valid: true,
})) }))
require.NoError(collect, listParams.SetLimit(domain.ServerListMaxLimit)) require.NoError(collect, listServersParams.SetLimit(domain.ServerListMaxLimit))
var allServers domain.Servers
for { for {
res, err := serverRepo.List(ctx, listParams) res, err := serverRepo.List(ctx, listServersParams)
require.NoError(collect, err) require.NoError(collect, err)
for _, s := range res.Servers() { for _, s := range res.Servers() {
assert.WithinDuration(collect, time.Now(), s.SnapshotCreatedAt(), time.Minute, s.Key())
assert.WithinDuration(collect, time.Now(), s.PlayerSnapshotsCreatedAt(), time.Minute, s.Key()) assert.WithinDuration(collect, time.Now(), s.PlayerSnapshotsCreatedAt(), time.Minute, s.Key())
assert.WithinDuration(collect, time.Now(), s.TribeSnapshotsCreatedAt(), time.Minute, s.Key()) assert.WithinDuration(collect, time.Now(), s.TribeSnapshotsCreatedAt(), time.Minute, s.Key())
} }
allServers = append(allServers, res.Servers()...)
if res.Next().IsZero() { if res.Next().IsZero() {
return break
} }
require.NoError(collect, listParams.SetCursor(res.Next())) require.NoError(collect, listServersParams.SetCursor(res.Next()))
} }
listSnapshotsParams := domain.NewListServerSnapshotsParams()
require.NoError(collect, listSnapshotsParams.SetSort([]domain.ServerSnapshotSort{
domain.ServerSnapshotSortServerKeyASC,
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortIDASC,
}))
require.NoError(collect, listSnapshotsParams.SetLimit(domain.ServerSnapshotListMaxLimit))
cnt := 0
for {
res, err := serverSnapshotRepo.List(ctx, listSnapshotsParams)
require.NoError(collect, err)
for _, ss := range res.ServerSnapshots() {
cnt++
msg := fmt.Sprintf("ServerKey=%s", ss.ServerKey())
idx := slices.IndexFunc(allServers, func(s domain.Server) bool {
return s.Key() == ss.ServerKey()
})
if !assert.GreaterOrEqual(
collect,
idx,
0,
msg,
) {
continue
}
server := allServers[idx]
assert.NotZero(collect, ss.ID(), msg)
assert.Equal(collect, server.Key(), ss.ServerKey(), msg)
assert.Equal(collect, server.NumVillages(), ss.NumVillages(), msg)
assert.WithinDuration(collect, time.Now(), ss.CreatedAt(), time.Minute, msg)
assert.WithinDuration(collect, time.Now(), ss.Date(), 24*time.Hour, msg)
}
if res.Next().IsZero() {
break
}
require.NoError(collect, listSnapshotsParams.SetCursor(res.Next()))
}
//nolint:testifylint
assert.Equal(collect, len(allServers), cnt)
}, 30*time.Second, 500*time.Millisecond, "servers") }, 30*time.Second, 500*time.Millisecond, "servers")
}() }()

View File

@ -10,10 +10,12 @@ import (
type ServerWatermillConsumer struct { type ServerWatermillConsumer struct {
svc *app.ServerService svc *app.ServerService
snapshotSvc *app.ServerSnapshotService
subscriber message.Subscriber subscriber message.Subscriber
logger watermill.LoggerAdapter logger watermill.LoggerAdapter
marshaler watermillmsg.Marshaler marshaler watermillmsg.Marshaler
cmdSyncTopic string cmdSyncTopic string
cmdCreateSnapshotsTopic string
eventServerSyncedTopic string eventServerSyncedTopic string
eventTribesSyncedTopic string eventTribesSyncedTopic string
eventPlayersSyncedTopic string eventPlayersSyncedTopic string
@ -25,10 +27,12 @@ type ServerWatermillConsumer struct {
func NewServerWatermillConsumer( func NewServerWatermillConsumer(
svc *app.ServerService, svc *app.ServerService,
snapshotSvc *app.ServerSnapshotService,
subscriber message.Subscriber, subscriber message.Subscriber,
logger watermill.LoggerAdapter, logger watermill.LoggerAdapter,
marshaler watermillmsg.Marshaler, marshaler watermillmsg.Marshaler,
cmdSyncTopic string, cmdSyncTopic string,
cmdCreateSnapshotsTopic string,
eventServerSyncedTopic string, eventServerSyncedTopic string,
eventTribesSyncedTopic string, eventTribesSyncedTopic string,
eventPlayersSyncedTopic string, eventPlayersSyncedTopic string,
@ -39,10 +43,12 @@ func NewServerWatermillConsumer(
) *ServerWatermillConsumer { ) *ServerWatermillConsumer {
return &ServerWatermillConsumer{ return &ServerWatermillConsumer{
svc: svc, svc: svc,
snapshotSvc: snapshotSvc,
subscriber: subscriber, subscriber: subscriber,
logger: logger, logger: logger,
marshaler: marshaler, marshaler: marshaler,
cmdSyncTopic: cmdSyncTopic, cmdSyncTopic: cmdSyncTopic,
cmdCreateSnapshotsTopic: cmdCreateSnapshotsTopic,
eventServerSyncedTopic: eventServerSyncedTopic, eventServerSyncedTopic: eventServerSyncedTopic,
eventTribesSyncedTopic: eventTribesSyncedTopic, eventTribesSyncedTopic: eventTribesSyncedTopic,
eventPlayersSyncedTopic: eventPlayersSyncedTopic, eventPlayersSyncedTopic: eventPlayersSyncedTopic,
@ -55,6 +61,12 @@ func NewServerWatermillConsumer(
func (c *ServerWatermillConsumer) Register(router *message.Router) { func (c *ServerWatermillConsumer) Register(router *message.Router) {
router.AddNoPublisherHandler("ServerConsumer.sync", c.cmdSyncTopic, c.subscriber, c.sync) router.AddNoPublisherHandler("ServerConsumer.sync", c.cmdSyncTopic, c.subscriber, c.sync)
router.AddNoPublisherHandler(
"ServerConsumer.createSnapshots",
c.cmdCreateSnapshotsTopic,
c.subscriber,
c.createSnapshots,
)
router.AddNoPublisherHandler( router.AddNoPublisherHandler(
"ServerConsumer.syncConfigAndInfo", "ServerConsumer.syncConfigAndInfo",
c.eventServerSyncedTopic, c.eventServerSyncedTopic,
@ -141,6 +153,32 @@ func (c *ServerWatermillConsumer) syncConfigAndInfo(msg *message.Message) error
return c.svc.SyncConfigAndInfo(msg.Context(), payload) return c.svc.SyncConfigAndInfo(msg.Context(), payload)
} }
func (c *ServerWatermillConsumer) 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)
}
func (c *ServerWatermillConsumer) updateNumTribes(msg *message.Message) error { func (c *ServerWatermillConsumer) updateNumTribes(msg *message.Message) error {
var rawPayload watermillmsg.TribesSyncedEventPayload var rawPayload watermillmsg.TribesSyncedEventPayload
@ -156,6 +194,7 @@ func (c *ServerWatermillConsumer) updateNumTribes(msg *message.Message) error {
rawPayload.ServerURL, rawPayload.ServerURL,
rawPayload.VersionCode, rawPayload.VersionCode,
rawPayload.NumTribes, rawPayload.NumTribes,
rawPayload.NumActiveTribes,
) )
if err != nil { if err != nil {
c.logger.Error("couldn't construct domain.TribesSyncedEventPayload", err, watermill.LogFields{ c.logger.Error("couldn't construct domain.TribesSyncedEventPayload", err, watermill.LogFields{
@ -182,6 +221,7 @@ func (c *ServerWatermillConsumer) updateNumPlayers(msg *message.Message) error {
rawPayload.ServerURL, rawPayload.ServerURL,
rawPayload.VersionCode, rawPayload.VersionCode,
rawPayload.NumPlayers, rawPayload.NumPlayers,
rawPayload.NumActivePlayers,
) )
if err != nil { if err != nil {
c.logger.Error("couldn't construct domain.PlayersSyncedEventPayload", err, watermill.LogFields{ c.logger.Error("couldn't construct domain.PlayersSyncedEventPayload", err, watermill.LogFields{

View File

@ -20,6 +20,7 @@ type apiHTTPHandler struct {
villageSvc *app.VillageService villageSvc *app.VillageService
ennoblementSvc *app.EnnoblementService ennoblementSvc *app.EnnoblementService
tribeChangeSvc *app.TribeChangeService tribeChangeSvc *app.TribeChangeService
serverSnapshotSvc *app.ServerSnapshotService
tribeSnapshotSvc *app.TribeSnapshotService tribeSnapshotSvc *app.TribeSnapshotService
playerSnapshotSvc *app.PlayerSnapshotService playerSnapshotSvc *app.PlayerSnapshotService
errorRenderer apiErrorRenderer errorRenderer apiErrorRenderer
@ -40,6 +41,7 @@ func NewAPIHTTPHandler(
villageSvc *app.VillageService, villageSvc *app.VillageService,
ennoblementSvc *app.EnnoblementService, ennoblementSvc *app.EnnoblementService,
tribeChangeSvc *app.TribeChangeService, tribeChangeSvc *app.TribeChangeService,
serverSnapshotSvc *app.ServerSnapshotService,
tribeSnapshotSvc *app.TribeSnapshotService, tribeSnapshotSvc *app.TribeSnapshotService,
playerSnapshotSvc *app.PlayerSnapshotService, playerSnapshotSvc *app.PlayerSnapshotService,
opts ...APIHTTPHandlerOption, opts ...APIHTTPHandlerOption,
@ -54,6 +56,7 @@ func NewAPIHTTPHandler(
villageSvc: villageSvc, villageSvc: villageSvc,
ennoblementSvc: ennoblementSvc, ennoblementSvc: ennoblementSvc,
tribeChangeSvc: tribeChangeSvc, tribeChangeSvc: tribeChangeSvc,
serverSnapshotSvc: serverSnapshotSvc,
tribeSnapshotSvc: tribeSnapshotSvc, tribeSnapshotSvc: tribeSnapshotSvc,
playerSnapshotSvc: playerSnapshotSvc, playerSnapshotSvc: playerSnapshotSvc,
openAPISchema: sync.OnceValues(func() (*openapi3.T, error) { openAPISchema: sync.OnceValues(func() (*openapi3.T, error) {

View File

@ -148,7 +148,6 @@ func (re apiErrorRenderer) domainErrorToAPIError(domainErr domain.Error) apiErro
var err error = domainErr var err error = domainErr
for { for {
var withPath domain.ErrorWithPath var withPath domain.ErrorWithPath
if !errors.As(err, &withPath) { if !errors.As(err, &withPath) {
break break
} }

View File

@ -0,0 +1,96 @@
package port
import (
"net/http"
"strconv"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/port/internal/apimodel"
)
const apiServerSnapshotSortMaxLength = 1
var apiServerSnapshotSortAllowedValues = []domain.ServerSnapshotSort{
domain.ServerSnapshotSortDateASC,
domain.ServerSnapshotSortDateDESC,
}
//nolint:gocyclo
func (h *apiHTTPHandler) ListServerServerSnapshots(
w http.ResponseWriter,
r *http.Request,
_ apimodel.VersionCodePathParam,
serverKey apimodel.ServerKeyPathParam,
params apimodel.ListServerServerSnapshotsParams,
) {
domainParams := domain.NewListServerSnapshotsParams()
if err := domainParams.SetSort([]domain.ServerSnapshotSort{domain.ServerSnapshotSortIDASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
if err := domainParams.SetServerKeys([]string{serverKey}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
if params.Sort != nil {
if err := domainParams.PrependSortString(
*params.Sort,
apiServerSnapshotSortAllowedValues,
apiServerSnapshotSortMaxLength,
); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
} else {
if err := domainParams.PrependSort([]domain.ServerSnapshotSort{domain.ServerSnapshotSortDateASC}); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
}
if params.Limit != nil {
if err := domainParams.SetLimit(*params.Limit); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
}
if params.Cursor != nil {
if err := domainParams.SetEncodedCursor(*params.Cursor); err != nil {
h.errorRenderer.withErrorPathFormatter(formatListServerSnapshotsErrorPath).render(w, r, err)
return
}
}
res, err := h.serverSnapshotSvc.ListWithRelations(r.Context(), domainParams)
if err != nil {
h.errorRenderer.render(w, r, err)
return
}
renderJSON(w, r, http.StatusOK, apimodel.NewListServerSnapshotsResponse(res))
}
func formatListServerSnapshotsErrorPath(segments []domain.ErrorPathSegment) []string {
if segments[0].Model != "ListServerSnapshotsParams" {
return nil
}
switch segments[0].Field {
case "cursor":
return []string{"$query", "cursor"}
case "limit":
return []string{"$query", "limit"}
case "sort":
path := []string{"$query", "sort"}
if segments[0].Index >= 0 {
path = append(path, strconv.Itoa(segments[0].Index))
}
return path
default:
return nil
}
}

View File

@ -0,0 +1,542 @@
package port_test
import (
"cmp"
"fmt"
"net/http"
"net/http/httptest"
"slices"
"strconv"
"strings"
"testing"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/domain/domaintest"
"gitea.dwysokinski.me/twhelp/core/internal/port/internal/apimodel"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const endpointListServerServerSnapshots = "/v2/versions/%s/servers/%s/snapshots"
func TestListServerServerSnapshots(t *testing.T) {
t.Parallel()
handler := newAPIHTTPHandler(t)
sss := getAllServerSnapshots(t, handler)
var server serverWithVersion
sssGroupedByServerKey := make(map[string][]serverSnapshotWithServer)
for _, ss := range sss {
key := ss.Server.Key
sssGroupedByServerKey[key] = append(sssGroupedByServerKey[key], ss)
}
currentMax := -1
for _, grouped := range sssGroupedByServerKey {
if l := len(grouped); l > currentMax && l > 0 {
currentMax = l
server = grouped[0].Server
}
}
tests := []struct {
name string
reqModifier func(t *testing.T, req *http.Request)
assertResp func(t *testing.T, req *http.Request, resp *http.Response)
}{
{
name: "OK: without params",
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.ServerSnapshot) int {
return cmp.Or(
a.Date.Compare(b.Date.Time),
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "OK: limit=1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "1")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.NotZero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: limit=1 cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "1")
req.URL.RawQuery = q.Encode()
resp := doCustomRequest(handler, req.Clone(req.Context()))
defer resp.Body.Close()
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
require.NotEmpty(t, body.Cursor.Next)
q.Set("cursor", body.Cursor.Next)
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.Equal(t, req.URL.Query().Get("cursor"), body.Cursor.Self)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
assert.Len(t, body.Data, limit)
},
},
{
name: "OK: sort=[date:DESC]",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("sort", "date:DESC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// body
body := decodeJSON[apimodel.ListServerSnapshotsResponse](t, resp.Body)
assert.Zero(t, body.Cursor.Next)
assert.NotZero(t, body.Cursor.Self)
assert.NotZero(t, body.Data)
assert.True(t, slices.IsSortedFunc(body.Data, func(a, b apimodel.ServerSnapshot) int {
return cmp.Or(
a.Date.Compare(b.Date.Time)*-1,
cmp.Compare(a.Id, b.Id),
)
}))
},
},
{
name: "ERR: limit is not an integer",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "asd")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: "invalid-param-format",
Message: fmt.Sprintf(
"error binding string parameter: strconv.ParseInt: parsing \"%s\": invalid syntax",
req.URL.Query().Get("limit"),
),
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: limit < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", "0")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MinGreaterEqualError{
Min: 1,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: fmt.Sprintf("ERR: limit > %d", domain.ServerSnapshotListMaxLimit),
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("limit", strconv.Itoa(domain.ServerSnapshotListMaxLimit+1))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
limit, err := strconv.Atoi(req.URL.Query().Get("limit"))
require.NoError(t, err)
domainErr := domain.MaxLessEqualError{
Max: domain.ServerSnapshotListMaxLimit,
Current: limit,
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
},
Path: []string{"$query", "limit"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) < 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", "")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: len(cursor) > 1000",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(1001))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 1000,
Current: len(req.URL.Query().Get("cursor")),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: invalid cursor",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Set("cursor", gofakeit.LetterN(100))
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, _ *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
var domainErr domain.Error
require.ErrorAs(t, domain.ErrInvalidCursor, &domainErr)
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Path: []string{"$query", "cursor"},
},
},
}, body)
},
},
{
name: "ERR: len(sort) > 1",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "date:DESC")
q.Add("sort", "date:ASC")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.LenOutOfRangeError{
Min: 0,
Max: 1,
Current: len(req.URL.Query()["sort"]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"current": float64(domainErr.Current),
"max": float64(domainErr.Max),
"min": float64(domainErr.Min),
},
Path: []string{"$query", "sort"},
},
},
}, body)
},
},
{
name: "ERR: invalid sort",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
q := req.URL.Query()
q.Add("sort", "date:")
req.URL.RawQuery = q.Encode()
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
domainErr := domain.UnsupportedSortStringError{
Sort: req.URL.Query()["sort"][0],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"sort": domainErr.Sort,
},
Path: []string{"$query", "sort", "0"},
},
},
}, body)
},
},
{
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(
endpointListServerServerSnapshots,
randInvalidVersionCode(t, handler),
server.Key,
)
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
pathSegments := strings.Split(req.URL.Path, "/")
require.Len(t, pathSegments, 7)
domainErr := domain.VersionNotFoundError{
VersionCode: pathSegments[3],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"code": domainErr.VersionCode,
},
},
},
}, body)
},
},
{
name: "ERR: server not found",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(
endpointListServerServerSnapshots,
server.Version.Code,
domaintest.RandServerKey(),
)
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
pathSegments := strings.Split(req.URL.Path, "/")
require.Len(t, pathSegments, 7)
domainErr := domain.ServerNotFoundError{
Key: pathSegments[5],
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: apimodel.ErrorCode(domainErr.Code()),
Message: domainErr.Error(),
Params: map[string]any{
"key": domainErr.Key,
},
},
},
}, body)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(
http.MethodGet,
fmt.Sprintf(endpointListServerServerSnapshots, server.Version.Code, server.Key),
nil,
)
if tt.reqModifier != nil {
tt.reqModifier(t, req)
}
resp := doCustomRequest(handler, req)
defer resp.Body.Close()
tt.assertResp(t, req, resp)
})
}
}
type serverSnapshotWithServer struct {
apimodel.ServerSnapshot
Server serverWithVersion
}
func getAllServerSnapshots(tb testing.TB, h http.Handler) []serverSnapshotWithServer {
tb.Helper()
servers := getAllServers(tb, h)
var sss []serverSnapshotWithServer
for _, s := range servers {
resp := doRequest(h, http.MethodGet, fmt.Sprintf(
endpointListServerServerSnapshots,
s.Version.Code,
s.Key,
), nil)
require.Equal(tb, http.StatusOK, resp.StatusCode)
for _, ts := range decodeJSON[apimodel.ListServerSnapshotsResponse](tb, resp.Body).Data {
sss = append(sss, serverSnapshotWithServer{
ServerSnapshot: ts,
Server: s,
})
}
_ = resp.Body.Close()
}
require.NotZero(tb, sss)
return sss
}

View File

@ -40,6 +40,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
villageRepo := adapter.NewVillageBunRepository(bunDB) villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB) ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB) tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB)
serverSnapshotRepo := adapter.NewServerSnapshotBunRepository(bunDB)
tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB) tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB)
playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB) playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB)
@ -51,6 +52,7 @@ func newAPIHTTPHandler(tb testing.TB, opts ...func(cfg *apiHTTPHandlerConfig)) h
app.NewVillageService(villageRepo, nil, nil), app.NewVillageService(villageRepo, nil, nil),
app.NewEnnoblementService(ennoblementRepo, nil, nil), app.NewEnnoblementService(ennoblementRepo, nil, nil),
app.NewTribeChangeService(tribeChangeRepo), app.NewTribeChangeService(tribeChangeRepo),
app.NewServerSnapshotService(serverSnapshotRepo, nil, nil),
app.NewTribeSnapshotService(tribeSnapshotRepo, nil, nil), app.NewTribeSnapshotService(tribeSnapshotRepo, nil, nil),
app.NewPlayerSnapshotService(playerSnapshotRepo, nil, nil), app.NewPlayerSnapshotService(playerSnapshotRepo, nil, nil),
cfg.options..., cfg.options...,

View File

@ -6,10 +6,14 @@ import (
func NewServer(s domain.Server) Server { func NewServer(s domain.Server) Server {
converted := Server{ converted := Server{
Key: s.Key(),
CreatedAt: s.CreatedAt(), CreatedAt: s.CreatedAt(),
Key: s.Key(),
NumActivePlayers: s.NumActivePlayers(),
NumActiveTribes: s.NumActiveTribes(),
NumBarbarianVillages: s.NumBarbarianVillages(), NumBarbarianVillages: s.NumBarbarianVillages(),
NumBonusVillages: s.NumBonusVillages(), NumBonusVillages: s.NumBonusVillages(),
NumInactivePlayers: s.NumInactivePlayers(),
NumInactiveTribes: s.NumInactiveTribes(),
NumPlayerVillages: s.NumPlayerVillages(), NumPlayerVillages: s.NumPlayerVillages(),
NumPlayers: s.NumPlayers(), NumPlayers: s.NumPlayers(),
NumTribes: s.NumTribes(), NumTribes: s.NumTribes(),

View File

@ -0,0 +1,43 @@
package apimodel
import (
"gitea.dwysokinski.me/twhelp/core/internal/domain"
oapitypes "github.com/oapi-codegen/runtime/types"
)
func NewServerSnapshot(withRelations domain.ServerSnapshotWithRelations) ServerSnapshot {
ss := withRelations.ServerSnapshot()
return ServerSnapshot{
Date: oapitypes.Date{Time: ss.Date()},
Id: ss.ID(),
NumActivePlayers: ss.NumActivePlayers(),
NumActiveTribes: ss.NumActiveTribes(),
NumBarbarianVillages: ss.NumBarbarianVillages(),
NumBonusVillages: ss.NumBonusVillages(),
NumInactivePlayers: ss.NumInactivePlayers(),
NumInactiveTribes: ss.NumInactiveTribes(),
NumPlayerVillages: ss.NumPlayerVillages(),
NumPlayers: ss.NumPlayers(),
NumTribes: ss.NumTribes(),
NumVillages: ss.NumVillages(),
Server: NewServerMeta(withRelations.Server()),
}
}
func NewListServerSnapshotsResponse(res domain.ListServerSnapshotsWithRelationsResult) ListServerSnapshotsResponse {
sss := res.ServerSnapshots()
resp := ListServerSnapshotsResponse{
Data: make([]ServerSnapshot, 0, len(sss)),
Cursor: Cursor{
Next: res.Next().Encode(),
Self: res.Self().Encode(),
},
}
for _, ss := range sss {
resp.Data = append(resp.Data, NewServerSnapshot(ss))
}
return resp
}

View File

@ -5,95 +5,95 @@
url: https://de180.die-staemme.de url: https://de180.die-staemme.de
open: false open: false
special: false special: false
num_players: 313 num_active_players: 313
num_tribes: 93 num_active_tribes: 93
num_villages: 47802 num_villages: 47802
num_player_villages: 47759 num_player_villages: 47759
num_barbarian_villages: 44 num_barbarian_villages: 44
num_bonus_villages: 0 num_bonus_villages: 0
created_at: 2022-01-19T12:00:54.000Z created_at: 2022-01-19T12:00:54.000Z
player_data_updated_at: 2022-01-19T12:00:54.000Z player_data_synced_at: 2022-01-19T12:00:54.000Z
player_snapshots_created_at: 2022-01-19T12:00:54.000Z player_snapshots_created_at: 2022-01-19T12:00:54.000Z
tribe_data_updated_at: 2022-01-19T12:00:54.000Z tribe_data_synced_at: 2022-01-19T12:00:54.000Z
tribe_snapshots_created_at: 2022-01-19T12:00:54.000Z tribe_snapshots_created_at: 2022-01-19T12:00:54.000Z
village_data_updated_at: 2022-01-19T12:00:54.000Z village_data_synced_at: 2022-01-19T12:00:54.000Z
ennoblement_data_updated_at: 2022-01-19T12:00:54.000Z ennoblement_data_synced_at: 2022-01-19T12:00:54.000Z
version_code: de version_code: de
- _id: de188 - _id: de188
key: de188 key: de188
url: https://de188.die-staemme.de url: https://de188.die-staemme.de
open: true open: true
special: false special: false
num_players: 180 num_active_players: 180
num_tribes: 76 num_active_tribes: 76
num_villages: 16180 num_villages: 16180
num_player_villages: 15000 num_player_villages: 15000
num_barbarian_villages: 1180 num_barbarian_villages: 1180
num_bonus_villages: 512 num_bonus_villages: 512
created_at: 2022-03-19T12:00:54.000Z created_at: 2022-03-19T12:00:54.000Z
player_data_updated_at: 2022-03-19T12:00:54.000Z player_data_synced_at: 2022-03-19T12:00:54.000Z
player_snapshots_created_at: 2022-03-19T12:00:54.000Z player_snapshots_created_at: 2022-03-19T12:00:54.000Z
tribe_data_updated_at: 2022-03-19T12:00:54.000Z tribe_data_synced_at: 2022-03-19T12:00:54.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:54.000Z tribe_snapshots_created_at: 2022-03-19T12:00:54.000Z
village_data_updated_at: 2022-03-19T12:00:54.000Z village_data_synced_at: 2022-03-19T12:00:54.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:54.000Z ennoblement_data_synced_at: 2022-03-19T12:00:54.000Z
version_code: de version_code: de
- _id: en113 - _id: en113
key: en113 key: en113
url: https://en113.tribalwars.net url: https://en113.tribalwars.net
open: false open: false
special: false special: false
num_players: 251 num_active_players: 251
num_tribes: 57 num_active_tribes: 57
num_villages: 41700 num_villages: 41700
num_player_villages: 41000 num_player_villages: 41000
num_barbarian_villages: 700 num_barbarian_villages: 700
num_bonus_villages: 1024 num_bonus_villages: 1024
created_at: 2021-04-02T16:01:25.000Z created_at: 2021-04-02T16:01:25.000Z
player_data_updated_at: 2021-04-02T16:01:25.000Z player_data_synced_at: 2021-04-02T16:01:25.000Z
player_snapshots_created_at: 2021-04-02T16:01:25.000Z player_snapshots_created_at: 2021-04-02T16:01:25.000Z
tribe_data_updated_at: 2021-04-02T16:01:25.000Z tribe_data_synced_at: 2021-04-02T16:01:25.000Z
tribe_snapshots_created_at: 2021-04-02T16:01:25.000Z tribe_snapshots_created_at: 2021-04-02T16:01:25.000Z
village_data_updated_at: 2021-04-02T16:01:25.000Z village_data_synced_at: 2021-04-02T16:01:25.000Z
ennoblement_data_updated_at: 2021-04-02T16:01:25.000Z ennoblement_data_synced_at: 2021-04-02T16:01:25.000Z
version_code: en version_code: en
- _id: it70 - _id: it70
key: it70 key: it70
url: https://it70.tribals.it url: https://it70.tribals.it
open: true open: true
special: false special: false
num_players: 1883 num_active_players: 1883
num_tribes: 101 num_active_tribes: 101
num_villages: 4882 num_villages: 4882
num_player_villages: 3200 num_player_villages: 3200
num_barbarian_villages: 1682 num_barbarian_villages: 1682
num_bonus_villages: 256 num_bonus_villages: 256
created_at: 2022-03-19T12:00:04.000Z created_at: 2022-03-19T12:00:04.000Z
player_data_updated_at: 2022-03-19T12:00:04.000Z player_data_synced_at: 2022-03-19T12:00:04.000Z
player_snapshots_created_at: 2022-03-19T12:00:04.000Z player_snapshots_created_at: 2022-03-19T12:00:04.000Z
tribe_data_updated_at: 2022-03-19T12:00:04.000Z tribe_data_synced_at: 2022-03-19T12:00:04.000Z
tribe_snapshots_created_at: 2022-03-19T12:00:04.000Z tribe_snapshots_created_at: 2022-03-19T12:00:04.000Z
village_data_updated_at: 2022-03-19T12:00:04.000Z village_data_synced_at: 2022-03-19T12:00:04.000Z
ennoblement_data_updated_at: 2022-03-19T12:00:04.000Z ennoblement_data_synced_at: 2022-03-19T12:00:04.000Z
version_code: it version_code: it
- _id: pl169 - _id: pl169
key: pl169 key: pl169
url: https://pl169.plemiona.pl url: https://pl169.plemiona.pl
open: true open: true
special: false special: false
num_players: 2001 num_active_players: 2001
num_tribes: 214 num_active_tribes: 214
num_villages: 49074 num_villages: 49074
num_player_villages: 48500 num_player_villages: 48500
num_barbarian_villages: 1574 num_barbarian_villages: 1574
num_bonus_villages: 2048 num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z created_at: 2022-03-19T12:01:39.000Z
player_data_updated_at: 2022-03-19T12:01:39.000Z player_data_synced_at: 2022-03-19T12:01:39.000Z
player_snapshots_created_at: 2022-03-19T12:01:39.000Z player_snapshots_created_at: 2022-03-19T12:01:39.000Z
tribe_data_updated_at: 2022-03-19T12:01:39.000Z tribe_data_synced_at: 2022-03-19T12:01:39.000Z
tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z
village_data_updated_at: 2022-03-19T12:01:39.000Z village_data_synced_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z ennoblement_data_synced_at: 2022-03-19T12:01:39.000Z
version_code: pl version_code: pl
- model: Tribe - model: Tribe
rows: rows:
@ -7393,6 +7393,64 @@
new_tribe_id: 2 new_tribe_id: 2
server_key: pl169 server_key: pl169
created_at: 2021-09-10T20:01:11.000Z created_at: 2021-09-10T20:01:11.000Z
- model: ServerSnapshot
rows:
- id: 10000
server_key: de188
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-01T05:15:25.154992Z
created_at: 2024-05-01T05:15:25.154994Z
- id: 10001
server_key: de188
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-02T05:15:25.154992Z
created_at: 2024-05-02T05:15:25.154994Z
- id: 20000
server_key: it70
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-03T05:15:25.154992Z
created_at: 2024-05-03T05:15:25.154994Z
- id: 20001
server_key: it70
num_players: 0
num_active_players: 180
num_inactive_players: 0
num_tribes: 0
num_active_tribes: 76
num_inactive_tribes: 0
num_villages: 16180
num_player_villages: 15000
num_barbarian_villages: 1180
num_bonus_villages: 512
date: 2024-05-04T05:15:25.154992Z
created_at: 2024-05-04T05:15:25.154994Z
- model: TribeSnapshot - model: TribeSnapshot
rows: rows:
- rank_att: 1 - rank_att: 1

View File

@ -4,8 +4,8 @@
url: https://pl181.plemiona.pl/ url: https://pl181.plemiona.pl/
open: false open: false
special: false special: false
num_players: 144 num_active_players: 144
num_tribes: 26 num_active_tribes: 26
num_villages: 59581 num_villages: 59581
num_player_villages: 58558 num_player_villages: 58558
num_barbarian_villages: 1023 num_barbarian_villages: 1023
@ -16,8 +16,8 @@
url: https://pl182.plemiona.pl/ url: https://pl182.plemiona.pl/
open: true open: true
special: false special: false
num_players: 144 num_active_players: 144
num_tribes: 26 num_active_tribes: 26
num_villages: 39702 num_villages: 39702
num_player_villages: 39697 num_player_villages: 39697
num_barbarian_villages: 5 num_barbarian_villages: 5
@ -28,20 +28,20 @@
url: https://pl185.plemiona.pl/ url: https://pl185.plemiona.pl/
open: false open: false
special: false special: false
num_players: 458 num_active_players: 458
num_tribes: 88 num_active_tribes: 88
num_villages: 84069 num_villages: 84069
num_player_villages: 83057 num_player_villages: 83057
num_barbarian_villages: 1012 num_barbarian_villages: 1012
num_bonus_villages: 13379 num_bonus_villages: 13379
created_at: created_at:
version_code: pl version_code: pl
player_data_updated_at: "{{ now }}" player_data_synced_at: "{{ now }}"
player_snapshots_created_at: "{{ now }}" player_snapshots_created_at: "{{ now }}"
tribe_data_updated_at: "{{ now }}" tribe_data_synced_at: "{{ now }}"
tribe_snapshots_created_at: "{{ now }}" tribe_snapshots_created_at: "{{ now }}"
village_data_updated_at: "{{ now }}" village_data_synced_at: "{{ now }}"
ennoblement_data_updated_at: "{{ now }}" ennoblement_data_synced_at: "{{ now }}"
- model: Ennoblement - model: Ennoblement
rows: rows:
# pl181 # pl181

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
url: https://us63.tribalwars.us url: https://us63.tribalwars.us
open: true open: true
special: false special: false
num_players: 1438 num_active_players: 1438
num_tribes: 125 num_active_tribes: 125
num_villages: 10048 num_villages: 10048
num_player_villages: 5386 num_player_villages: 5386
num_barbarian_villages: 4662 num_barbarian_villages: 4662

View File

@ -5,19 +5,23 @@
url: https://pl169.plemiona.pl url: https://pl169.plemiona.pl
open: true open: true
special: false special: false
num_players: 2001 num_players: 10000
num_tribes: 214 num_active_players: 2001
num_inactive_players: 7999
num_tribes: 500
num_active_tribes: 214
num_inactive_tribes: 286
num_villages: 49074 num_villages: 49074
num_player_villages: 48500 num_player_villages: 48500
num_barbarian_villages: 1574 num_barbarian_villages: 1574
num_bonus_villages: 2048 num_bonus_villages: 2048
created_at: 2022-03-19T12:01:39.000Z created_at: 2022-03-19T12:01:39.000Z
player_data_updated_at: 2022-03-19T12:01:39.000Z player_data_synced_at: 2022-03-19T12:01:39.000Z
player_snapshots_created_at: 2022-03-19T12:01:39.000Z player_snapshots_created_at: 2022-03-19T12:01:39.000Z
tribe_data_updated_at: 2022-03-19T12:01:39.000Z tribe_data_synced_at: 2022-03-19T12:01:39.000Z
tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z tribe_snapshots_created_at: 2022-03-19T12:01:39.000Z
village_data_updated_at: 2022-03-19T12:01:39.000Z village_data_synced_at: 2022-03-19T12:01:39.000Z
ennoblement_data_updated_at: 2022-03-19T12:01:39.000Z ennoblement_data_synced_at: 2022-03-19T12:01:39.000Z
version_code: pl version_code: pl
- model: Tribe - model: Tribe
rows: rows:

View File

@ -3,8 +3,9 @@ package watermillmsg
import "net/url" import "net/url"
type PlayersSyncedEventPayload struct { type PlayersSyncedEventPayload struct {
ServerKey string `json:"serverKey"` ServerKey string `json:"serverKey"`
ServerURL *url.URL `json:"serverUrl"` ServerURL *url.URL `json:"serverUrl"`
VersionCode string `json:"versionCode"` VersionCode string `json:"versionCode"`
NumPlayers int `json:"numPlayers"` NumPlayers int `json:"numPlayers"`
NumActivePlayers int `json:"numActivePlayers"`
} }

View File

@ -3,8 +3,9 @@ package watermillmsg
import "net/url" import "net/url"
type TribesSyncedEventPayload struct { type TribesSyncedEventPayload struct {
ServerKey string `json:"serverKey"` ServerKey string `json:"serverKey"`
ServerURL *url.URL `json:"serverUrl"` ServerURL *url.URL `json:"serverUrl"`
VersionCode string `json:"versionCode"` VersionCode string `json:"versionCode"`
NumTribes int `json:"numTribes"` NumTribes int `json:"numTribes"`
NumActiveTribes int `json:"numActiveTribes"`
} }

View File

@ -39,8 +39,6 @@ spec:
key: rabbitmq-connection-string key: rabbitmq-connection-string
- name: AUTO_MAX_PROCS - name: AUTO_MAX_PROCS
value: "true" value: "true"
- name: API_OPENAPI_SERVERS
value: https://twhelp.app,https://tribalwarshelp.com
resources: resources:
requests: requests:
cpu: 20m cpu: 20m

View File

@ -29,3 +29,5 @@ spec:
key: rabbitmq-connection-string key: rabbitmq-connection-string
- name: AUTO_MAX_PROCS - name: AUTO_MAX_PROCS
value: "true" value: "true"
- name: API_OPENAPI_SERVERS
value: https://twhelp.app,https://tribalwarshelp.com