diff options
15 files changed, 226 insertions, 14 deletions
diff --git a/build.gradle.kts b/build.gradle.kts index 171c811..8377205 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,9 @@ import com.github.gmazzo.buildconfig.BuildConfigExtension import java.io.ByteArrayOutputStream plugins { - kotlin("jvm") version "2.0.20" apply false + val kotlinVersion = "2.0.20" + kotlin("jvm") version kotlinVersion apply false + kotlin("plugin.serialization") version kotlinVersion apply false id("com.github.gmazzo.buildconfig") version "5.5.0" apply false id("ledger-globals") id("com.github.johnrengelman.shadow") version "8.1.1" apply false diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt index d1294f7..33727de 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt @@ -1,10 +1,31 @@ package moe.nea.ledger.database +import moe.nea.ledger.database.sql.IntoSelectable +import moe.nea.ledger.database.sql.Selectable +import java.sql.PreparedStatement + class Column<T> @Deprecated("Use Table.column instead") constructor( val table: Table, val name: String, val type: DBType<T> -) { +) : IntoSelectable<T> { + override fun asSelectable() = object : Selectable<T> { + override fun asSql(): String { + return qualifiedSqlName + } + + override val dbType: DBType<T> + get() = this@Column.type + + override fun guessColumn(): Column<T>? { + return this@Column + } + + override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int { + return startIndex + } + } + val sqlName get() = "`$name`" val qualifiedSqlName get() = table.sqlName + "." + sqlName }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt index 7829fbb..e58eef4 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt @@ -3,19 +3,22 @@ package moe.nea.ledger.database import moe.nea.ledger.database.sql.ANDExpression import moe.nea.ledger.database.sql.BooleanExpression import moe.nea.ledger.database.sql.Clause +import moe.nea.ledger.database.sql.IntoSelectable import moe.nea.ledger.database.sql.Join import moe.nea.ledger.database.sql.SQLQueryComponent import moe.nea.ledger.database.sql.SQLQueryGenerator.concatToFilledPreparedStatement +import moe.nea.ledger.database.sql.Selectable import java.sql.Connection class Query( val connection: Connection, - val selectedColumns: MutableList<Column<*>>, + val selectedColumns: MutableList<Selectable<*>>, var table: Table, var limit: UInt? = null, var skip: UInt? = null, val joins: MutableList<Join> = mutableListOf(), val conditions: MutableList<BooleanExpression> = mutableListOf(), + var distinct: Boolean = false, // var order: OrderClause?= null, ) : Iterable<ResultRow> { fun join(table: Table, on: Clause): Query { @@ -28,8 +31,10 @@ class Query( return this } - fun select(vararg columns: Column<*>): Query { - selectedColumns.addAll(columns) + fun select(vararg columns: IntoSelectable<*>): Query { + for (column in columns) { + this.selectedColumns.add(column.asSelectable()) + } return this } @@ -39,16 +44,29 @@ class Query( return this } + fun distinct(): Query { + this.distinct = true + return this + } + fun limit(limit: UInt): Query { this.limit = limit return this } override fun iterator(): Iterator<ResultRow> { - val columnSelections = selectedColumns.joinToString { it.qualifiedSqlName } val elements = mutableListOf( - SQLQueryComponent.standalone("SELECT $columnSelections FROM ${table.sqlName}"), + SQLQueryComponent.standalone("SELECT"), ) + if (distinct) + elements.add(SQLQueryComponent.standalone("DISTINCT")) + selectedColumns.forEachIndexed { idx, it -> + elements.add(it) + if (idx != selectedColumns.lastIndex) { + elements.add(SQLQueryComponent.standalone(",")) + } + } + elements.add(SQLQueryComponent.standalone("FROM ${table.sqlName}")) elements.addAll(joins) if (conditions.any()) { elements.add(SQLQueryComponent.standalone("WHERE")) @@ -84,7 +102,7 @@ class Query( } hasAdvanced = false return ResultRow(selectedColumns.withIndex().associate { - it.value to it.value.type.get(results, it.index + 1) + it.value to it.value.dbType.get(results, it.index + 1) }) } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt index d92f913..6715f27 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt @@ -1,9 +1,22 @@ package moe.nea.ledger.database -class ResultRow(val columnValues: Map<Column<*>, *>) { +import moe.nea.ledger.database.sql.Selectable + +class ResultRow(val selectableValues: Map<Selectable<*>, *>) { + val columnValues = selectableValues.mapNotNull { + val col = it.key.guessColumn() ?: return@mapNotNull null + col to it.value + }.toMap() + operator fun <T> get(column: Column<T>): T { val value = columnValues[column] ?: error("Invalid column ${column.name}. Only ${columnValues.keys.joinToString { it.name }} are available.") return value as T } + + operator fun <T> get(column: Selectable<T>): T { + val value = selectableValues[column] + ?: error("Invalid selectable ${column}. Only ${selectableValues.keys} are available.") + return value as T + } }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt index c136f48..61dc8f0 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt @@ -98,6 +98,6 @@ abstract class Table(val name: String) { } fun selectAll(connection: Connection): Query { - return Query(connection, columns.toMutableList(), this) + return Query(connection, columns.mapTo(mutableListOf()) { it.asSelectable() }, this) } }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt new file mode 100644 index 0000000..0068f6b --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt @@ -0,0 +1,5 @@ +package moe.nea.ledger.database.sql + +interface IntoSelectable<T> { + fun asSelectable(): Selectable<T> +}
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt index be81ff2..2eb54fd 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt @@ -6,14 +6,14 @@ import java.sql.PreparedStatement object SQLQueryGenerator { fun List<SQLQueryComponent>.concatToFilledPreparedStatement(connection: Connection): PreparedStatement { - var query = "" + val query = StringBuilder() for (element in this) { if (query.isNotEmpty()) { - query += " " + query.append(" ") } - query += element.asSql() + query.append(element.asSql()) } - val statement = connection.prepareAndLog(query) + val statement = connection.prepareAndLog(query.toString()) var index = 1 for (element in this) { val nextIndex = element.appendToStatement(statement, index) diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt new file mode 100644 index 0000000..a95b66b --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt @@ -0,0 +1,17 @@ +package moe.nea.ledger.database.sql + +import moe.nea.ledger.database.Column +import moe.nea.ledger.database.DBType + +/** + * Something that can be selected. Like a column, or an expression thereof + */ +interface Selectable<T> : SQLQueryComponent, IntoSelectable<T> { + override fun asSelectable(): Selectable<T> { + return this + } + + val dbType: DBType<T> + fun guessColumn(): Column<T>? +} + diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts new file mode 100644 index 0000000..87f613a --- /dev/null +++ b/server/core/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + application +} + +val ktor_version = "3.0.3" + +dependencies { + implementation(platform("io.ktor:ktor-bom:$ktor_version")) + implementation("io.ktor:ktor-server-netty") + implementation("io.ktor:ktor-server-status-pages") + implementation("io.ktor:ktor-server-content-negotiation") + implementation("io.ktor:ktor-server-openapi") + implementation("io.ktor:ktor-serialization-kotlinx-json") + implementation("io.ktor:ktor-server-compression") + implementation(project(":database:impl")) + + runtimeOnly("ch.qos.logback:logback-classic:1.5.16") + runtimeOnly("org.xerial:sqlite-jdbc:3.45.3.0") +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} +application { + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment", + "-Dledger.databasefolder=${project(":mod").file("run/money-ledger").absoluteFile}") + mainClass.set("moe.nea.ledger.server.core.ApplicationKt") +} diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt new file mode 100644 index 0000000..0ea6ed3 --- /dev/null +++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt @@ -0,0 +1,34 @@ +package moe.nea.ledger.server.core + +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.netty.EngineMain +import io.ktor.server.plugins.compression.Compression +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import moe.nea.ledger.database.Database +import moe.nea.ledger.server.core.api.apiRouting +import java.io.File + +fun main(args: Array<String>) { + EngineMain.main(args) +} + + +fun Application.module() { + install(Compression) + install(ContentNegotiation) { + json() +// cbor() + } + val database = Database(File(System.getProperty("ledger.databasefolder"))) + database.loadAndUpgrade() + routing { + route("/api") { + this.apiRouting(database) + } + } +} + diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt new file mode 100644 index 0000000..264f74b --- /dev/null +++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt @@ -0,0 +1,25 @@ +package moe.nea.ledger.server.core.api + +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.Route +import io.ktor.server.routing.Routing +import io.ktor.server.routing.get +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.Database +import moe.nea.ledger.server.core.Profile + +fun Route.apiRouting(database: Database) { + get("/") { + call.respondText("K") + } + get("/profiles") { + val profiles = DBLogEntry.from(database.connection) + .select(DBLogEntry.playerId, DBLogEntry.profileId) + .distinct() + .map { + Profile(it[DBLogEntry.playerId], it[DBLogEntry.profileId]) + } + call.respond(profiles) + } +} diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt new file mode 100644 index 0000000..a27a729 --- /dev/null +++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt @@ -0,0 +1,30 @@ +@file:UseSerializers(UUIDSerializer::class) + +package moe.nea.ledger.server.core + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import java.util.UUID + +object UUIDSerializer : KSerializer<UUID> { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("LedgerUUID", PrimitiveKind.STRING) + + override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } + + override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: UUID) { + encoder.encodeString(value.toString()) + } +} + +@Serializable +data class Profile( + val playerId: UUID, + val profileId: UUID, +)
\ No newline at end of file diff --git a/server/core/src/main/resources/application.conf b/server/core/src/main/resources/application.conf new file mode 100644 index 0000000..386ffd3 --- /dev/null +++ b/server/core/src/main/resources/application.conf @@ -0,0 +1,10 @@ +ktor { + application { + modules = [ + moe.nea.ledger.server.core.ApplicationKt.module + ] + } + deployment { + port = 8080 + } +}
\ No newline at end of file diff --git a/server/core/test-requests/test-hello.http b/server/core/test-requests/test-hello.http new file mode 100644 index 0000000..3ddf352 --- /dev/null +++ b/server/core/test-requests/test-hello.http @@ -0,0 +1,5 @@ +### GET request to example server +GET localhost:8080/ + +### GET profiles +GET localhost:8080/api/profiles
\ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 19f404d..d34446f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,4 +31,5 @@ include("database:core") include("database:impl") include("basetypes") include("mod") +include("server:core") includeBuild("build-src") |