feat: initial db persistence
This commit is contained in:
parent
e8abfb18eb
commit
2c4995f9d7
9 changed files with 342 additions and 46 deletions
4
.env.example
Normal file
4
.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DB_URL=jdbc:postgresql://localhost:5432/homepage
|
||||
DB_MIGRATION=src/main/resources/db/migration
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
184
build.gradle.kts
184
build.gradle.kts
|
|
@ -1,14 +1,42 @@
|
|||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import io.github.klahap.dotenv.DotEnvBuilder
|
||||
import org.flywaydb.gradle.task.FlywayMigrateTask
|
||||
import org.gradle.api.JavaVersion.VERSION_21
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
import kotlin.io.path.Path
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.2.20"
|
||||
val env = DotEnvBuilder.dotEnv {
|
||||
addFile("$rootDir/.env")
|
||||
addSystemEnv()
|
||||
}
|
||||
|
||||
id("gg.jte.gradle") version "3.2.1"
|
||||
id("com.gradleup.shadow") version "9.3.0"
|
||||
val envDbUrl: String = env["DB_URL"] ?: ""
|
||||
val envDbMigration: String = env["DB_MIGRATIONS"] ?: "src/main/resources/db/migration"
|
||||
val envDbUsername: String = env["DB_USERNAME"] ?: ""
|
||||
val envDbPassword: String = env["DB_PASSWORD"] ?: ""
|
||||
|
||||
val generatedResourcesDirectory = "${layout.buildDirectory.get()}/generated-resources"
|
||||
val generatedSourcesDirectory = "${layout.buildDirectory.get()}/generated-src"
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.dotenv.plugin)
|
||||
alias(libs.plugins.jte)
|
||||
alias(libs.plugins.flyway)
|
||||
alias(libs.plugins.jooq.codegen.gradle)
|
||||
alias(libs.plugins.taskinfo)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(21))
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
buildscript {
|
||||
|
|
@ -18,32 +46,20 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath(libs.postgresql)
|
||||
classpath(libs.flyway.database.postgresql)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(21))
|
||||
}
|
||||
}
|
||||
|
||||
jte {
|
||||
sourceDirectory.set(Path("src/main/kte"))
|
||||
targetDirectory.set(Path("${layout.buildDirectory.get()}/classes/jte"))
|
||||
|
||||
precompile()
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
resources.srcDir("${layout.buildDirectory.get()}/classes/jte")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
resources.srcDir("$generatedResourcesDirectory/jte")
|
||||
kotlin.srcDir("$generatedSourcesDirectory/jooq")
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<KotlinJvmCompile>().configureEach {
|
||||
dependsOn("jooqCodegen")
|
||||
|
||||
compilerOptions {
|
||||
allWarningsAsErrors = false
|
||||
jvmTarget.set(JVM_21)
|
||||
|
|
@ -55,6 +71,14 @@ tasks {
|
|||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
withType<FlywayMigrateTask> {
|
||||
dependsOn("initDb")
|
||||
}
|
||||
|
||||
named("precompileJte") {
|
||||
dependsOn("compileKotlin")
|
||||
}
|
||||
|
||||
named<ShadowJar>("shadowJar") {
|
||||
manifest {
|
||||
attributes("Main-Class" to "at.dokkae.homepage.HomepageKt")
|
||||
|
|
@ -62,13 +86,19 @@ tasks {
|
|||
|
||||
dependsOn("precompileJte")
|
||||
|
||||
from("${layout.buildDirectory.get()}/classes/jte")
|
||||
mustRunAfter("flywayMigrate", "jooqCodegen")
|
||||
|
||||
from("$generatedResourcesDirectory/jte")
|
||||
|
||||
archiveFileName.set("app.jar")
|
||||
|
||||
exclude("META-INF/*.RSA", "META-INF/*.SF", "META-INF/*.DSA")
|
||||
}
|
||||
|
||||
register("buildDocker") {
|
||||
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = VERSION_21
|
||||
targetCompatibility = VERSION_21
|
||||
|
|
@ -76,17 +106,101 @@ tasks {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform("org.http4k:http4k-bom:6.23.1.0"))
|
||||
implementation("org.http4k:http4k-client-okhttp")
|
||||
implementation("org.http4k:http4k-core")
|
||||
implementation("org.http4k:http4k-server-jetty")
|
||||
implementation("org.http4k:http4k-template-jte")
|
||||
implementation("org.http4k:http4k-web-htmx")
|
||||
implementation("gg.jte:jte-kotlin:3.2.1")
|
||||
testImplementation("org.http4k:http4k-testing-approval")
|
||||
testImplementation("org.http4k:http4k-testing-hamkrest")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.0")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-engine:6.0.0")
|
||||
testImplementation("org.junit.platform:junit-platform-launcher:6.0.0")
|
||||
implementation(platform(libs.http4k.bom))
|
||||
|
||||
implementation(libs.dotenv)
|
||||
|
||||
implementation(libs.bundles.http4k)
|
||||
implementation(libs.jte.kotlin)
|
||||
implementation(libs.bundles.database)
|
||||
|
||||
testImplementation(libs.bundles.testing)
|
||||
|
||||
jooqCodegen(libs.jooq.meta)
|
||||
jooqCodegen(libs.jooq.postgres)
|
||||
}
|
||||
|
||||
// ========== JTE Templating ==========
|
||||
jte {
|
||||
sourceDirectory.set(Path("src/main/kte"))
|
||||
targetDirectory.set(Path("$generatedResourcesDirectory/jte"))
|
||||
precompile()
|
||||
}
|
||||
|
||||
// ========== FlyWay ==========
|
||||
flyway {
|
||||
url = envDbUrl
|
||||
user = envDbUsername
|
||||
password = envDbPassword
|
||||
locations = arrayOf("filesystem:$envDbMigration")
|
||||
baselineOnMigrate = true
|
||||
validateMigrationNaming = true
|
||||
}
|
||||
|
||||
tasks.register("initDb") {
|
||||
doFirst {
|
||||
println("Database Configuration:")
|
||||
println(" Raw URL from env: $envDbUrl")
|
||||
println(" Resolved URL: $envDbUrl")
|
||||
println(" Migrations: $envDbMigration")
|
||||
println(" Credentials:")
|
||||
println(" Username: $envDbUsername")
|
||||
println(" Password: ${"*".repeat(envDbPassword.length)}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("flywayMigrate") {
|
||||
finalizedBy("jooqCodegen")
|
||||
}
|
||||
|
||||
// ========== Jooq ==========
|
||||
jooq {
|
||||
configuration {
|
||||
logging = org.jooq.meta.jaxb.Logging.WARN
|
||||
|
||||
jdbc {
|
||||
driver = "org.postgresql.Driver"
|
||||
url = envDbUrl
|
||||
user = envDbUsername
|
||||
password = envDbPassword
|
||||
}
|
||||
|
||||
generator {
|
||||
name = "org.jooq.codegen.KotlinGenerator"
|
||||
|
||||
database {
|
||||
name = "org.jooq.meta.postgres.PostgresDatabase"
|
||||
inputSchema = "public"
|
||||
|
||||
// SQLite specific configuration
|
||||
includes = ".*"
|
||||
excludes = """
|
||||
flyway_.*|
|
||||
pg_.*|
|
||||
information_schema.*
|
||||
""".trimMargin().replace("\n", "")
|
||||
}
|
||||
|
||||
generate {
|
||||
// Recommended settings for Kotlin
|
||||
isDeprecated = false
|
||||
isRecords = true
|
||||
isImmutablePojos = true
|
||||
isFluentSetters = true
|
||||
isKotlinNotNullRecordAttributes = true
|
||||
isKotlinNotNullPojoAttributes = true
|
||||
isKotlinNotNullInterfaceAttributes = true
|
||||
isPojosAsKotlinDataClasses = true
|
||||
}
|
||||
|
||||
target {
|
||||
packageName = "at.dokkae.homepage.generated.jooq"
|
||||
directory = "$generatedSourcesDirectory/jooq"
|
||||
}
|
||||
|
||||
strategy {
|
||||
name = "org.jooq.codegen.DefaultGeneratorStrategy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.configuration-cache=false
|
||||
83
gradle/libs.versions.toml
Normal file
83
gradle/libs.versions.toml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
[versions]
|
||||
kotlin = "2.2.20"
|
||||
shadow = "9.3.0"
|
||||
dotenv-plugin = "1.1.3"
|
||||
dotenv = "6.5.1"
|
||||
http4k = "6.23.1.0"
|
||||
jte = "3.2.1"
|
||||
flyway = "11.19.0"
|
||||
jooq = "3.20.10"
|
||||
junit = "6.0.0"
|
||||
postgresql = "42.7.7"
|
||||
taskinfo = "3.0.0"
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
|
||||
dotenv-plugin = { id = "io.github.klahap.dotenv", version.ref = "dotenv-plugin" }
|
||||
jte = { id = "gg.jte.gradle", version.ref = "jte" }
|
||||
flyway = { id = "org.flywaydb.flyway", version.ref = "flyway" }
|
||||
jooq-codegen-gradle = { id = "org.jooq.jooq-codegen-gradle", version.ref = "jooq" }
|
||||
taskinfo = { id = "org.barfuin.gradle.taskinfo", version.ref = "taskinfo" }
|
||||
|
||||
[bundles]
|
||||
http4k = [
|
||||
"http4k-client-okhttp",
|
||||
"http4k-core",
|
||||
"http4k-server-jetty",
|
||||
"http4k-template-jte",
|
||||
"http4k-web-htmx"
|
||||
]
|
||||
|
||||
testing = [
|
||||
"http4k-testing-approval",
|
||||
"http4k-testing-hamkrest",
|
||||
"junit-jupiter-api",
|
||||
"junit-jupiter-engine",
|
||||
"junit-platform-launcher"
|
||||
]
|
||||
|
||||
database = [
|
||||
"flyway-core",
|
||||
"jooq",
|
||||
"jooq-meta",
|
||||
"jooq-codegen",
|
||||
"jooq-postgres"
|
||||
]
|
||||
|
||||
[libraries]
|
||||
# Environment Management
|
||||
dotenv = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" }
|
||||
|
||||
# HTTP4K Platform (BOM)
|
||||
http4k-bom = { module = "org.http4k:http4k-bom", version.ref = "http4k" }
|
||||
|
||||
# HTTP4K Dependencies
|
||||
http4k-client-okhttp = { module = "org.http4k:http4k-client-okhttp" }
|
||||
http4k-core = { module = "org.http4k:http4k-core" }
|
||||
http4k-server-jetty = { module = "org.http4k:http4k-server-jetty" }
|
||||
http4k-template-jte = { module = "org.http4k:http4k-template-jte" }
|
||||
http4k-web-htmx = { module = "org.http4k:http4k-web-htmx" }
|
||||
|
||||
# JTE Templating
|
||||
jte-kotlin = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
|
||||
|
||||
# Database Driver
|
||||
postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" }
|
||||
|
||||
# Flyway
|
||||
flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" }
|
||||
flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway"}
|
||||
|
||||
# Jooq
|
||||
jooq = { module = "org.jooq:jooq", version.ref = "jooq" }
|
||||
jooq-meta = { module = "org.jooq:jooq-meta", version.ref = "jooq" }
|
||||
jooq-codegen = { module = "org.jooq:jooq-codegen", version.ref = "jooq" }
|
||||
jooq-postgres = { module = "org.jooq:jooq-postgres-extensions", version.ref = "jooq" }
|
||||
|
||||
# Testing
|
||||
http4k-testing-approval = { module = "org.http4k:http4k-testing-approval" }
|
||||
http4k-testing-hamkrest = { module = "org.http4k:http4k-testing-hamkrest" }
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
|
||||
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
package at.dokkae.homepage
|
||||
|
||||
import at.dokkae.homepage.extensions.Precompiled
|
||||
import at.dokkae.homepage.repository.MessageRepository
|
||||
import at.dokkae.homepage.repository.impls.JooqMessageRepository
|
||||
import at.dokkae.homepage.templates.Index
|
||||
import io.github.cdimascio.dotenv.dotenv
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.core.Method.*
|
||||
import org.http4k.core.Response
|
||||
|
|
@ -22,6 +25,9 @@ import org.http4k.sse.SseResponse
|
|||
import org.http4k.template.JTETemplates
|
||||
import org.http4k.template.ViewModel
|
||||
import org.jetbrains.kotlin.backend.common.push
|
||||
import org.jooq.SQLDialect
|
||||
import org.jooq.impl.DSL
|
||||
import java.sql.DriverManager
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
|
@ -30,22 +36,32 @@ import kotlin.concurrent.thread
|
|||
data class Message(
|
||||
val author: String,
|
||||
val content: String,
|
||||
val createdAt: Instant = Instant.now(),
|
||||
|
||||
val id: UUID = UUID.randomUUID(),
|
||||
val createdAt: Instant = Instant.now(),
|
||||
val updatedAt: Instant? = null
|
||||
) : ViewModel {
|
||||
init {
|
||||
require(author.length <= 31) { "Author must be 31 characters or less" }
|
||||
require(content.length <= 255) { "Content must be 255 characters or less" }
|
||||
}
|
||||
|
||||
|
||||
override fun template(): String = "partials/Message"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val messages = CopyOnWriteArrayList<Message>()
|
||||
val subscribers = CopyOnWriteArrayList<Sse>()
|
||||
val renderer = JTETemplates().Precompiled("build/classes/jte")
|
||||
val env = dotenv()
|
||||
|
||||
messages.push(Message("Kurisu", "Hello World!"))
|
||||
messages.push(Message("Violet", "Haii Kurisu!"))
|
||||
val connection = DriverManager.getConnection(env["DB_URL"], env["DB_USERNAME"], env["DB_PASSWORD"])
|
||||
val dslContext = DSL.using(connection, SQLDialect.POSTGRES)
|
||||
val messageRepository: MessageRepository = JooqMessageRepository(dslContext)
|
||||
|
||||
val subscribers = CopyOnWriteArrayList<Sse>()
|
||||
val renderer = JTETemplates().Precompiled("build/generated-resources/jte")
|
||||
|
||||
val indexHandler: HttpHandler = {
|
||||
Response(Status.OK).body(renderer(Index(messages)))
|
||||
Response(Status.OK).body(renderer(Index(messageRepository.findAll())))
|
||||
}
|
||||
|
||||
val sse = sse(
|
||||
|
|
@ -72,12 +88,12 @@ fun main() {
|
|||
val msg = Message(author, message)
|
||||
val sseMsg = SseMessage.Data(renderer(msg))
|
||||
|
||||
messages.push(msg)
|
||||
messageRepository.save(msg)
|
||||
subscribers.forEach {
|
||||
thread { it.send(sseMsg) }
|
||||
}
|
||||
|
||||
Response(Status.OK)
|
||||
Response(Status.CREATED)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package at.dokkae.homepage.repository
|
||||
|
||||
import at.dokkae.homepage.Message
|
||||
|
||||
interface MessageRepository {
|
||||
fun save(message: Message): Message
|
||||
fun findAll(): List<Message>
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package at.dokkae.homepage.repository.impls
|
||||
|
||||
import at.dokkae.homepage.Message
|
||||
import at.dokkae.homepage.generated.jooq.tables.records.MessageRecord
|
||||
import at.dokkae.homepage.generated.jooq.tables.references.MESSAGE
|
||||
import at.dokkae.homepage.repository.MessageRepository
|
||||
import org.jooq.DSLContext
|
||||
import org.jooq.impl.DSL
|
||||
|
||||
class JooqMessageRepository(
|
||||
private val dslContext: DSLContext
|
||||
) : MessageRepository {
|
||||
override fun save(message: Message): Message = dslContext.transactionResult { config ->
|
||||
val ctx = DSL.using(config)
|
||||
|
||||
ctx.insertInto(MESSAGE)
|
||||
.set(MESSAGE.ID, message.id)
|
||||
.set(MESSAGE.AUTHOR, message.author)
|
||||
.set(MESSAGE.CONTENT, message.content)
|
||||
.onDuplicateKeyUpdate()
|
||||
.set(MESSAGE.AUTHOR, message.author)
|
||||
.set(MESSAGE.CONTENT, message.content)
|
||||
.returning()
|
||||
.fetchOne()!!
|
||||
.toMessage()
|
||||
}
|
||||
|
||||
override fun findAll(): List<Message> = dslContext.selectFrom(MESSAGE)
|
||||
.orderBy(MESSAGE.CREATED_AT.desc())
|
||||
.fetch { it.toMessage() }
|
||||
|
||||
|
||||
private fun MessageRecord.toMessage(): Message = Message(
|
||||
id = this.id,
|
||||
author = this.author,
|
||||
content = this.content,
|
||||
createdAt = this.createdAt!!.toInstant(),
|
||||
updatedAt = this.updatedAt?.toInstant(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
create or replace function handle_timestamps()
|
||||
returns trigger as
|
||||
$$
|
||||
begin
|
||||
if lower(tg_op) = 'insert' then
|
||||
new.created_at = coalesce(new.created_at, current_timestamp);
|
||||
new.updated_at = coalesce(new.updated_at, current_timestamp);
|
||||
elsif lower(tg_op) = 'update' then
|
||||
if new.created_at is distinct from old.created_at then
|
||||
raise exception 'Direct modification of created_at is not allowed.';
|
||||
end if;
|
||||
|
||||
new.updated_at = current_timestamp;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
13
src/main/resources/db/migration/V002__add_message_table.sql
Normal file
13
src/main/resources/db/migration/V002__add_message_table.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
create table message (
|
||||
id uuid not null primary key,
|
||||
author varchar(31) not null,
|
||||
content varchar(255) not null,
|
||||
|
||||
created_at timestamptz not null default current_timestamp,
|
||||
updated_at timestamptz
|
||||
);
|
||||
|
||||
create trigger handle_message_timestamps
|
||||
before insert or update on message
|
||||
for each row
|
||||
execute function handle_timestamps();
|
||||
Loading…
Add table
Add a link
Reference in a new issue