Skip to content

Commit

Permalink
PostgreSQL and Exposed integration
Browse files Browse the repository at this point in the history
  • Loading branch information
HarisHoulis committed Feb 24, 2024
1 parent c5d42de commit 2a2f9d3
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 0 deletions.
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ dependencies {
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion"


implementation 'org.jetbrains.exposed:exposed-core:0.47.0'
implementation 'org.jetbrains.exposed:exposed-jdbc:0.47.0'
implementation 'org.jetbrains.exposed:exposed-java-time:0.47.0'
implementation "org.postgresql:postgresql:42.5.3"
implementation "com.zaxxer:HikariCP:5.0.1"

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
Expand Down
19 changes: 19 additions & 0 deletions db/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: '3'
services:
local-database:
image: "postgres:15.2"
env_file:
- postgres.env # configure postgres
ports:
- "5432:5432"
volumes:
- pg-volume:/var/lib/postgresql/data/
test-database:
image: "postgres:15.2"
env_file:
- postgres.env
ports:
- "5433:5432"
# no volumes for test
volumes:
pg-volume: { }
3 changes: 3 additions & 0 deletions db/postgres.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POSTGRES_USER=gilded
POSTGRES_PASSWORD=rose
POSTGRES_DB=gilded-rose
5 changes: 5 additions & 0 deletions db/start-db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
BASEDIR=$(dirname "$0")

docker-compose --file ${BASEDIR}/docker-compose.yml up
164 changes: 164 additions & 0 deletions src/test/kotlin/com/gildedrose/persistence/ItemsTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.gildedrose.com.gildedrose.persistence

import com.gildedrose.domain.ID
import com.gildedrose.domain.Item
import com.gildedrose.domain.NonBlankString
import com.gildedrose.domain.Quality
import com.gildedrose.itemForTest
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.javatime.date
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.postgresql.ds.PGSimpleDataSource
import strikt.api.expectCatching
import strikt.api.expectThat
import strikt.assertions.isA
import strikt.assertions.isEmpty
import strikt.assertions.isEqualTo
import strikt.assertions.isFailure
import java.time.LocalDate

private val dataSource = PGSimpleDataSource().apply {
user = "gilded"
password = "rose"
databaseName = "gilded-rose"
}

private val db = Database.connect(dataSource)

@Disabled("Can't run on CI for now")
internal class ItemsTests {

private val item1 = itemForTest("id-1", "name", LocalDate.of(2023, 2, 14), 42)
private val item2 = itemForTest("id-2", "another name", null, 99)

private val items = Items()

@BeforeEach
fun setUp() {
transaction(db) {
SchemaUtils.drop(ItemsTable)
SchemaUtils.createMissingTablesAndColumns(ItemsTable)
}
}

@Test
fun `add item`() {
transaction(db) {
expectThat(items.all()).isEmpty()
}

transaction(db) {
items.add(item1)
items.add(item2)
expectThat(items.all()).isEqualTo(listOf(item1, item2))
}

}

@Test
fun `find by id`() {
transaction(db) {
items.add(item1)
items.add(item2)
expectThat(items.all()).isEqualTo(listOf(item1, item2))
}

transaction(db) {
expectThat(items.findById(ID<Item>("no-such-id")!!))
.isEqualTo(null)
expectThat(items.findById(ID<Item>("id-1")!!))
.isEqualTo(item1)
}

}

@Test
fun update() {
transaction(db) {
items.add(item1)
items.add(item2)
}

val updatedItem = item1.copy(name = NonBlankString("new name")!!)
transaction(db) {
items.update(updatedItem)
}

transaction(db) {
expectThat(items.findById(item1.id))
.isEqualTo(updatedItem)
}

transaction(db) {
expectCatching {
items.update(item1.copy(id = ID("no-such-id")!!))
}.isFailure().isA<IllegalStateException>()
}
}
}

class Items {
fun all(): List<Item> {
return ItemsTable.all()
}

fun add(item: Item) {
ItemsTable.insert(item)
}

fun findById(id: ID<Item>): Item? {
val items = ItemsTable.selectAll().where {
ItemsTable.id eq id.toString()
}.map(ResultRow::toItem)
if (items.size > 1)
TODO("Handle duplicate IDs")
else
return items.firstOrNull()
}

fun update(item: Item) {
val rowsChanged = ItemsTable.update({ ItemsTable.id eq item.id.toString() }) {
it[id] = item.id.toString()
it[name] = item.name.toString()
it[sellByDate] = item.sellByDate
it[quality] = item.quality.valueInt
}
check(rowsChanged == 1)
}
}

object ItemsTable : Table() {
val id: Column<String> = varchar("id", 100)
val name: Column<String> = varchar("name", 100)
val sellByDate: Column<LocalDate?> = date("sellByDate").nullable()
val quality: Column<Int> = integer("quality")
}

fun ItemsTable.insert(item: Item) {
insert {
it[id] = item.id.toString()
it[name] = item.name.toString()
it[sellByDate] = item.sellByDate
it[quality] = item.quality.valueInt
}
}

fun ItemsTable.all() = selectAll().map(ResultRow::toItem)

private fun ResultRow.toItem() = Item(
ID(this[ItemsTable.id]) ?: error("Could not parse id ${this[ItemsTable.id]}"),
NonBlankString(this[ItemsTable.name]) ?: error("Invalid name ${this[ItemsTable.name]}"),
this[ItemsTable.sellByDate],
Quality(this[ItemsTable.quality]) ?: error("Invalid quality ${this[ItemsTable.quality]}"),
)

0 comments on commit 2a2f9d3

Please sign in to comment.