+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"reflection": [
|
||||
{ "type": "scala.Tuple1[]" },
|
||||
{ "type": "scala.Tuple2[]" },
|
||||
{ "type": "scala.Tuple3[]" },
|
||||
{ "type": "scala.Tuple4[]" },
|
||||
{ "type": "scala.Tuple5[]" },
|
||||
{ "type": "scala.Tuple6[]" },
|
||||
{ "type": "scala.Tuple7[]" },
|
||||
{ "type": "scala.Tuple8[]" },
|
||||
{ "type": "scala.Tuple9[]" },
|
||||
{ "type": "scala.Tuple10[]" },
|
||||
{ "type": "scala.Tuple11[]" },
|
||||
{ "type": "scala.Tuple12[]" },
|
||||
{ "type": "scala.Tuple13[]" },
|
||||
{ "type": "scala.Tuple14[]" },
|
||||
{ "type": "scala.Tuple15[]" },
|
||||
{ "type": "scala.Tuple16[]" },
|
||||
{ "type": "scala.Tuple17[]" },
|
||||
{ "type": "scala.Tuple18[]" },
|
||||
{ "type": "scala.Tuple19[]" },
|
||||
{ "type": "scala.Tuple20[]" },
|
||||
{ "type": "scala.Tuple21[]" },
|
||||
{ "type": "scala.Tuple22[]" },
|
||||
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
quarkus:
|
||||
application:
|
||||
name: nowchess-store
|
||||
http.port: 8085
|
||||
config:
|
||||
yaml:
|
||||
enabled: true
|
||||
redis:
|
||||
hosts: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
|
||||
datasource:
|
||||
db-kind: postgresql
|
||||
username: ${DB_USER:nowchess}
|
||||
password: ${DB_PASSWORD:nowchess}
|
||||
jdbc:
|
||||
url: ${DB_URL:jdbc:postgresql://localhost:5432/nowchess}
|
||||
hibernate-orm:
|
||||
database:
|
||||
generation: update
|
||||
|
||||
nowchess:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
prefix: ${REDIS_PREFIX:nowchess}
|
||||
|
||||
"%test":
|
||||
quarkus:
|
||||
datasource:
|
||||
db-kind: h2
|
||||
username: sa
|
||||
password: ""
|
||||
jdbc:
|
||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
hibernate-orm:
|
||||
database:
|
||||
generation: drop-and-create
|
||||
nowchess:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
prefix: test-store
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.nowchess.store.config
|
||||
|
||||
import com.fasterxml.jackson.core.Version
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class JacksonConfig extends ObjectMapperCustomizer:
|
||||
def customize(mapper: ObjectMapper): Unit =
|
||||
mapper.registerModule(new DefaultScalaModule() {
|
||||
override def version(): Version =
|
||||
// scalafix:off DisableSyntax.null
|
||||
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||
// scalafix:on DisableSyntax.null
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.nowchess.store.config
|
||||
|
||||
import de.nowchess.api.dto.GameWritebackEventDto
|
||||
import de.nowchess.store.domain.GameRecord
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
|
||||
@RegisterForReflection(
|
||||
targets = Array(
|
||||
classOf[GameRecord],
|
||||
classOf[GameWritebackEventDto],
|
||||
),
|
||||
)
|
||||
class NativeReflectionConfig
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.nowchess.store.config
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@ApplicationScoped
|
||||
class RedisConfig:
|
||||
// scalafix:off DisableSyntax.var
|
||||
@ConfigProperty(name = "nowchess.redis.prefix", defaultValue = "nowchess")
|
||||
var prefix: String = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
@@ -0,0 +1,98 @@
|
||||
package de.nowchess.store.domain
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase
|
||||
import jakarta.persistence.*
|
||||
import scala.compiletime.uninitialized
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "game_records",
|
||||
indexes = Array(
|
||||
new Index(name = "idx_game_records_white_id", columnList = "whiteId"),
|
||||
new Index(name = "idx_game_records_black_id", columnList = "blackId"),
|
||||
),
|
||||
)
|
||||
class GameRecord extends PanacheEntityBase:
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
var gameId: String = uninitialized
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
var fen: String = uninitialized
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
var pgn: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var moveCount: Int = 0
|
||||
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var updatedAt: Instant = uninitialized
|
||||
|
||||
// Player info
|
||||
@Column(nullable = false)
|
||||
var whiteId: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var whiteName: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var blackId: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var blackName: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var mode: String = uninitialized
|
||||
|
||||
// Time control
|
||||
@Column
|
||||
var limitSeconds: java.lang.Integer = uninitialized
|
||||
|
||||
@Column
|
||||
var incrementSeconds: java.lang.Integer = uninitialized
|
||||
|
||||
@Column
|
||||
var daysPerMove: java.lang.Integer = uninitialized
|
||||
|
||||
// Clock state
|
||||
@Column
|
||||
var whiteRemainingMs: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var blackRemainingMs: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var incrementMs: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var clockLastTickAt: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var clockMoveDeadline: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var clockActiveColor: String = uninitialized
|
||||
|
||||
// Game meta
|
||||
@Column(nullable = false)
|
||||
var resigned: Boolean = false
|
||||
|
||||
@Column
|
||||
var pendingDrawOffer: String = uninitialized
|
||||
|
||||
@Column
|
||||
var pendingTakebackOffer: String = uninitialized
|
||||
|
||||
// Game result
|
||||
@Column
|
||||
var result: String = uninitialized
|
||||
|
||||
@Column
|
||||
var terminationReason: String = uninitialized
|
||||
// scalafix:on
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package de.nowchess.store.redis
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import de.nowchess.api.dto.GameWritebackEventDto
|
||||
import de.nowchess.store.service.GameWritebackService
|
||||
import io.quarkus.redis.datasource.RedisDataSource
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import scala.compiletime.uninitialized
|
||||
import scala.util.Try
|
||||
import java.util.function.Consumer
|
||||
|
||||
@ApplicationScoped
|
||||
class GameWritebackStreamListener:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var redis: RedisDataSource = uninitialized
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@Inject var writebackService: GameWritebackService = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
@PostConstruct
|
||||
def startListening(): Unit =
|
||||
val handler: Consumer[String] = json =>
|
||||
Try(objectMapper.readValue(json, classOf[GameWritebackEventDto])).toOption
|
||||
.foreach(writebackService.writeBack)
|
||||
redis.pubsub(classOf[String]).subscribe("game-writeback", handler)
|
||||
()
|
||||
@@ -0,0 +1,46 @@
|
||||
package de.nowchess.store.repository
|
||||
|
||||
import de.nowchess.store.domain.GameRecord
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.persistence.EntityManager
|
||||
import scala.compiletime.uninitialized
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
@ApplicationScoped
|
||||
class GameRecordRepository:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var em: EntityManager = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
def findByGameId(gameId: String): Option[GameRecord] =
|
||||
Option(em.find(classOf[GameRecord], gameId))
|
||||
|
||||
def persist(record: GameRecord): Unit =
|
||||
em.persist(record)
|
||||
|
||||
def merge(record: GameRecord): Unit =
|
||||
em.merge(record)
|
||||
|
||||
def findByPlayerId(playerId: String, offset: Int, limit: Int): List[GameRecord] =
|
||||
em.createQuery(
|
||||
"SELECT g FROM GameRecord g WHERE g.whiteId = :id OR g.blackId = :id AND g.result != null ORDER BY g.updatedAt DESC",
|
||||
classOf[GameRecord],
|
||||
).setParameter("id", playerId)
|
||||
.setFirstResult(offset)
|
||||
.setMaxResults(limit)
|
||||
.getResultList
|
||||
.asScala
|
||||
.toList
|
||||
|
||||
def findByPlayerIdRunning(playerId: String, offset: Int, limit: Int): List[GameRecord] =
|
||||
em.createQuery(
|
||||
"SELECT g FROM GameRecord g WHERE g.whiteId = :id OR g.blackId = :id AND g.result = null ORDER BY g.updatedAt DESC",
|
||||
classOf[GameRecord],
|
||||
).setParameter("id", playerId)
|
||||
.setFirstResult(offset)
|
||||
.setMaxResults(limit)
|
||||
.getResultList
|
||||
.asScala
|
||||
.toList
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.nowchess.store.resource
|
||||
|
||||
import de.nowchess.store.repository.GameRecordRepository
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.{MediaType, Response}
|
||||
import jakarta.ws.rs.DefaultValue
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@Path("/game")
|
||||
@ApplicationScoped
|
||||
class StoreGameResource:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var repository: GameRecordRepository = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
@GET
|
||||
@Path("/{gameId}")
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def getGame(@PathParam("gameId") gameId: String): Response =
|
||||
repository
|
||||
.findByGameId(gameId)
|
||||
.fold(Response.status(404).build())(r => Response.ok(r).build())
|
||||
|
||||
@GET
|
||||
@Path("/running/{playerId}")
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def getRunning(
|
||||
@PathParam("playerId") playerId: String,
|
||||
@QueryParam("offset") @DefaultValue("0") offset: Int,
|
||||
@QueryParam("limit") @DefaultValue("20") limit: Int,
|
||||
): Response =
|
||||
Response.ok(repository.findByPlayerIdRunning(playerId, offset, limit)).build()
|
||||
|
||||
@GET
|
||||
@Path("/history/{playerId}")
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def getHistory(
|
||||
@PathParam("playerId") playerId: String,
|
||||
@QueryParam("offset") @DefaultValue("0") offset: Int,
|
||||
@QueryParam("limit") @DefaultValue("20") limit: Int,
|
||||
): Response =
|
||||
Response.ok(repository.findByPlayerId(playerId, offset, limit)).build()
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.nowchess.store.service
|
||||
|
||||
import de.nowchess.api.dto.GameWritebackEventDto
|
||||
import de.nowchess.store.domain.GameRecord
|
||||
import de.nowchess.store.repository.GameRecordRepository
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.transaction.Transactional
|
||||
|
||||
import scala.compiletime.uninitialized
|
||||
import java.time.Instant
|
||||
|
||||
@ApplicationScoped
|
||||
class GameWritebackService:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var repository: GameRecordRepository = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
@Transactional
|
||||
def writeBack(event: GameWritebackEventDto): Unit =
|
||||
repository.findByGameId(event.gameId) match
|
||||
case None =>
|
||||
val record = new GameRecord
|
||||
record.gameId = event.gameId
|
||||
record.fen = event.fen
|
||||
record.pgn = event.pgn
|
||||
record.moveCount = event.moveCount
|
||||
record.whiteId = event.whiteId
|
||||
record.whiteName = event.whiteName
|
||||
record.blackId = event.blackId
|
||||
record.blackName = event.blackName
|
||||
record.mode = event.mode
|
||||
record.resigned = event.resigned
|
||||
record.limitSeconds = event.limitSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
record.incrementSeconds = event.incrementSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
record.daysPerMove = event.daysPerMove.map(java.lang.Integer.valueOf).orNull
|
||||
record.whiteRemainingMs = event.whiteRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
record.blackRemainingMs = event.blackRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
record.incrementMs = event.incrementMs.map(java.lang.Long.valueOf).orNull
|
||||
record.clockLastTickAt = event.clockLastTickAt.map(java.lang.Long.valueOf).orNull
|
||||
record.clockMoveDeadline = event.clockMoveDeadline.map(java.lang.Long.valueOf).orNull
|
||||
record.clockActiveColor = event.clockActiveColor.orNull
|
||||
record.pendingDrawOffer = event.pendingDrawOffer.orNull
|
||||
record.result = event.result.orNull
|
||||
record.terminationReason = event.terminationReason.orNull
|
||||
record.createdAt = Instant.now()
|
||||
record.updatedAt = Instant.now()
|
||||
repository.persist(record)
|
||||
case Some(r) if event.moveCount > r.moveCount || event.pgn != r.pgn =>
|
||||
r.fen = event.fen
|
||||
r.pgn = event.pgn
|
||||
r.moveCount = event.moveCount
|
||||
r.whiteId = event.whiteId
|
||||
r.whiteName = event.whiteName
|
||||
r.blackId = event.blackId
|
||||
r.blackName = event.blackName
|
||||
r.mode = event.mode
|
||||
r.resigned = event.resigned
|
||||
r.limitSeconds = event.limitSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
r.incrementSeconds = event.incrementSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
r.daysPerMove = event.daysPerMove.map(java.lang.Integer.valueOf).orNull
|
||||
r.whiteRemainingMs = event.whiteRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
r.blackRemainingMs = event.blackRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
r.incrementMs = event.incrementMs.map(java.lang.Long.valueOf).orNull
|
||||
r.clockLastTickAt = event.clockLastTickAt.map(java.lang.Long.valueOf).orNull
|
||||
r.clockMoveDeadline = event.clockMoveDeadline.map(java.lang.Long.valueOf).orNull
|
||||
r.clockActiveColor = event.clockActiveColor.orNull
|
||||
r.pendingDrawOffer = event.pendingDrawOffer.orNull
|
||||
r.pendingTakebackOffer = event.pendingTakebackRequest.orNull
|
||||
r.result = event.result.orNull
|
||||
r.terminationReason = event.terminationReason.orNull
|
||||
r.updatedAt = Instant.now()
|
||||
repository.merge(r)
|
||||
case _ => ()
|
||||
Reference in New Issue
Block a user