forked from Minecraft/Stonks
241 lines
9.1 KiB
Kotlin
241 lines
9.1 KiB
Kotlin
package xyz.etztech.stonks.api
|
|
|
|
import io.javalin.Javalin
|
|
import java.time.Duration
|
|
import java.time.Instant
|
|
import kotlinx.coroutines.*
|
|
import kotlinx.serialization.*
|
|
import kotlinx.serialization.json.Json
|
|
import org.jetbrains.exposed.sql.*
|
|
import org.jetbrains.exposed.sql.transactions.*
|
|
import xyz.etztech.stonks.dsl.AggregateStatistics
|
|
import xyz.etztech.stonks.dsl.LiveStatistics
|
|
import xyz.etztech.stonks.dsl.Players
|
|
import xyz.etztech.stonks.dsl.Statistics
|
|
|
|
private var statisticsCache = ""
|
|
|
|
private var statisticsCacheTimestamp = Instant.now()
|
|
|
|
private var playersCache = ""
|
|
|
|
private var playersCacheTimestamp = Instant.now()
|
|
|
|
fun initApiServer(apiServerPort: Int, database: Database) {
|
|
val app =
|
|
Javalin.create { config ->
|
|
config.enableCorsForAllOrigins()
|
|
config.addStaticFiles("spa")
|
|
config.addSinglePageRoot("", "spa/index.html")
|
|
config.enforceSsl = false
|
|
config.ignoreTrailingSlashes = true
|
|
}
|
|
.start(apiServerPort)
|
|
|
|
app.get("api/statistics") { ctx ->
|
|
run {
|
|
if (statisticsCache.isEmpty() or
|
|
(Duration.between(Instant.now(), statisticsCacheTimestamp) >
|
|
Duration.ofHours(1))
|
|
) {
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
|
|
val statistics =
|
|
LiveStatistics.slice(LiveStatistics.type, LiveStatistics.name)
|
|
.selectAll()
|
|
.groupBy(LiveStatistics.type, LiveStatistics.name)
|
|
.map {
|
|
Statistic(it[LiveStatistics.type], it[LiveStatistics.name])
|
|
}
|
|
|
|
statisticsCache = Json.encodeToString(statistics)
|
|
}
|
|
}
|
|
|
|
ctx.result(statisticsCache)
|
|
}
|
|
}
|
|
|
|
app.get("api/statistics/:type/:name") { ctx ->
|
|
run {
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
val statistics =
|
|
LiveStatistics.slice(
|
|
LiveStatistics.type,
|
|
LiveStatistics.name,
|
|
LiveStatistics.playerId,
|
|
LiveStatistics.value,
|
|
LiveStatistics.rank
|
|
)
|
|
.select {
|
|
LiveStatistics.type.eq(ctx.pathParam("type")) and
|
|
LiveStatistics.name.eq(ctx.pathParam("name"))
|
|
}
|
|
.orderBy(LiveStatistics.rank, SortOrder.ASC)
|
|
.map {
|
|
StatisticValue(
|
|
it[LiveStatistics.playerId],
|
|
it[LiveStatistics.type],
|
|
it[LiveStatistics.name],
|
|
it[LiveStatistics.value],
|
|
it[LiveStatistics.rank]
|
|
)
|
|
}
|
|
|
|
ctx.result(Json { prettyPrint = true }.encodeToString(statistics))
|
|
}
|
|
}
|
|
}
|
|
|
|
app.get("api/players") { ctx ->
|
|
run {
|
|
if (playersCache.isEmpty() or
|
|
(Duration.between(Instant.now(), playersCacheTimestamp) >
|
|
Duration.ofHours(1))
|
|
) {
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
|
|
val players =
|
|
LiveStatistics.leftJoin(Players, { playerId }, { id })
|
|
.slice(LiveStatistics.playerId, Players.name)
|
|
.selectAll()
|
|
.groupBy(LiveStatistics.playerId)
|
|
.map { Player(it[LiveStatistics.playerId], it[Players.name]) }
|
|
playersCache = Json.encodeToString(players)
|
|
}
|
|
}
|
|
|
|
ctx.result(playersCache)
|
|
}
|
|
}
|
|
|
|
app.get("api/players/:playerId") { ctx ->
|
|
run {
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
|
|
val statistics =
|
|
LiveStatistics.slice(
|
|
LiveStatistics.type,
|
|
LiveStatistics.name,
|
|
LiveStatistics.playerId,
|
|
LiveStatistics.value,
|
|
LiveStatistics.rank
|
|
)
|
|
.select { LiveStatistics.playerId.eq(ctx.pathParam("playerId")) }
|
|
.map {
|
|
StatisticValue(
|
|
it[LiveStatistics.playerId],
|
|
it[LiveStatistics.type],
|
|
it[LiveStatistics.name],
|
|
it[LiveStatistics.value],
|
|
it[LiveStatistics.rank]
|
|
)
|
|
}
|
|
|
|
ctx.result(Json { prettyPrint = true }.encodeToString(statistics))
|
|
}
|
|
}
|
|
}
|
|
|
|
app.get("api/players/:playerId/:type/:name") { ctx ->
|
|
run {
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
val statistics =
|
|
Statistics.slice(
|
|
Statistics.type,
|
|
Statistics.name,
|
|
Statistics.playerId,
|
|
Statistics.timestamp,
|
|
Statistics.value
|
|
)
|
|
.select {
|
|
Statistics.playerId.eq(ctx.pathParam("playerId")) and
|
|
Statistics.type.eq(ctx.pathParam("type")) and
|
|
Statistics.name.eq(ctx.pathParam("name"))
|
|
}
|
|
.orderBy(Statistics.timestamp, SortOrder.DESC)
|
|
.map {
|
|
HistoricalStatisticValue(
|
|
it[Statistics.playerId],
|
|
it[Statistics.type],
|
|
it[Statistics.name],
|
|
it[Statistics.timestamp].toString(),
|
|
it[Statistics.value]
|
|
)
|
|
}
|
|
|
|
ctx.result(Json { prettyPrint = true }.encodeToString(statistics))
|
|
}
|
|
}
|
|
}
|
|
|
|
app.get("api/aggregates") { ctx ->
|
|
run {
|
|
transaction(database) {
|
|
addLogger(StdOutSqlLogger)
|
|
|
|
val maxValueExpr = AggregateStatistics.value.max()
|
|
|
|
val aggregates =
|
|
AggregateStatistics.slice(
|
|
AggregateStatistics.type,
|
|
AggregateStatistics.name,
|
|
maxValueExpr
|
|
)
|
|
.selectAll()
|
|
.groupBy(AggregateStatistics.type, AggregateStatistics.name)
|
|
.map {
|
|
AggregateValue(
|
|
it[AggregateStatistics.type],
|
|
it[AggregateStatistics.name],
|
|
it[maxValueExpr]!!
|
|
)
|
|
}
|
|
|
|
ctx.result(Json { prettyPrint = true }.encodeToString(aggregates))
|
|
}
|
|
}
|
|
}
|
|
|
|
app.get("api/*") { ctx -> ctx.status(404) }
|
|
|
|
println("Javalin web server started")
|
|
}
|
|
|
|
@Serializable data class Statistic(val type: String, val name: String)
|
|
|
|
@Serializable data class Player(val id: String, val name: String?)
|
|
|
|
@Serializable
|
|
data class StatisticValue(
|
|
val playerId: String,
|
|
val type: String,
|
|
val name: String,
|
|
val value: Long,
|
|
val rank: Int
|
|
)
|
|
|
|
@Serializable
|
|
data class HistoricalStatisticValue(
|
|
val playerId: String,
|
|
val type: String,
|
|
val name: String,
|
|
val timestamp: String,
|
|
val value: Long
|
|
)
|
|
|
|
@Serializable data class AggregateValue(val type: String, val name: String, val value: Long)
|
|
|
|
@Serializable
|
|
data class HistoricalAggregateValue(
|
|
val type: String,
|
|
val name: String,
|
|
val timestamp: String,
|
|
val value: Long
|
|
)
|