Skip to content

Commit

Permalink
Add kales-cli db migrate task (#12)
Browse files Browse the repository at this point in the history
* Add database migrations core

* Replace harmonica code with jar. Create DbMigrateTask

* Create db/migrate dir. Add PackageName tests

* Add db migrate test

* jitpack is no longer needed

* Nits

* New base class + fix test
  • Loading branch information
felipecsl authored Feb 25, 2019
1 parent 03f4ddc commit cc4b790
Show file tree
Hide file tree
Showing 24 changed files with 458 additions and 106 deletions.
4 changes: 2 additions & 2 deletions activemodel/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
}

dependencies {
implementation project(":activerecord")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
api "org.jdbi:jdbi3-core:$jdbiVersion"
api "org.jdbi:jdbi3-kotlin:$jdbiVersion"
api "org.jdbi:jdbi3-postgres:$jdbiVersion"
implementation 'org.yaml:snakeyaml:1.23'
testImplementation "com.google.truth:truth:$truthVersion"
testRuntime 'com.h2database:h2:1.4.197'
testRuntime "com.h2database:h2:$h2DBVersion"
}
22 changes: 2 additions & 20 deletions activemodel/src/main/kotlin/kales/ApplicationRecord.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package kales

import kales.migrations.KalesDatabaseConfig
import org.jdbi.v3.core.Handle
import org.jdbi.v3.core.Jdbi
import org.jdbi.v3.core.h2.H2DatabasePlugin
import org.jdbi.v3.core.kotlin.KotlinPlugin
import org.jdbi.v3.core.kotlin.mapTo
import org.jdbi.v3.postgres.PostgresPlugin
import org.yaml.snakeyaml.Yaml

abstract class ApplicationRecord {
companion object {
Expand All @@ -15,29 +15,11 @@ abstract class ApplicationRecord {
.installPlugin(H2DatabasePlugin())
.installPlugin(KotlinPlugin())

@Suppress("UNCHECKED_CAST")
private fun dbConnectionString(): String {
val yaml = Yaml()
val stream = ApplicationRecord::class.java.classLoader.getResourceAsStream("database.yml")
val data = yaml.load<Map<String, Any>>(stream)
// TODO handle muliple environments
val devData = data["development"] as? Map<String, String> ?: throwMissingField("development")
val adapter = devData["adapter"] ?: throwMissingField("adapter")
val host = devData["host"] ?: throwMissingField("host")
val database = devData["database"] ?: throwMissingField("database")
return if (adapter == "h2") {
"jdbc:$adapter:$host:$database"
} else {
val username = devData["username"] ?: ""
val password = devData["password"] ?: ""
"jdbc:$adapter://$host/$database?user=$username&password=$password"
}
return KalesDatabaseConfig.fromDatabaseYml(stream).toString()
}

private fun throwMissingField(name: String): Nothing =
throw IllegalArgumentException(
"Please set a value for the field '$name' in the file database.yml")

inline fun <reified T : ApplicationRecord> allRecords(): List<T> {
useJdbi {
val tableName = toTableName<T>()
Expand Down
5 changes: 5 additions & 0 deletions activerecord/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,10 @@ apply from: "$rootDir/gradle/publish.gradle"

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation 'org.yaml:snakeyaml:1.23'
api files("libs/harmonica-core-1.1.18.jar")
// TODO these should come transitively from harmonica
implementation 'org.reflections:reflections:0.9.11'
testImplementation "com.google.truth:truth:$truthVersion"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion"
}
Binary file added activerecord/libs/harmonica-core-1.1.18.jar
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package kales.migrations

import org.yaml.snakeyaml.Yaml
import java.io.InputStream

/** Represents a Kales database config as defined in a database.yml file */
data class KalesDatabaseConfig(
val environment: String,
val adapter: String,
val host: String,
val database: String,
val username: String,
val password: String
) {
override fun toString(): String {
return if (adapter == "h2") {
"jdbc:$adapter:$host:$database"
} else {
"jdbc:$adapter://$host/$database?user=$username&password=$password"
}
}

companion object {
@Suppress("UNCHECKED_CAST")
fun fromDatabaseYml(fileStream: InputStream): KalesDatabaseConfig {
val yaml = Yaml()
val data = yaml.load<Map<String, Any>>(fileStream)
// TODO handle muliple environments
val devData = data["development"] as? Map<String, String> ?: throwMissingField("development")
val adapter = devData["adapter"] ?: throwMissingField("adapter")
val host = devData["host"] ?: throwMissingField("host")
val database = devData["database"] ?: throwMissingField("database")
val username = devData["username"] ?: ""
val password = devData["password"] ?: ""
return KalesDatabaseConfig("development", adapter, host, database, username, password)
}

private fun throwMissingField(name: String): Nothing =
throw IllegalArgumentException(
"Please set a value for the field '$name' in the file database.yml")
}
}
5 changes: 5 additions & 0 deletions activerecord/src/main/kotlin/kales/migrations/Migration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package kales.migrations

import com.improve_future.harmonica.core.AbstractMigration

abstract class Migration : AbstractMigration()
1 change: 1 addition & 0 deletions gradle/deps.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ ext {
truthVersion = '0.42'
ktorVersion = '1.1.2'
jdbiVersion = '3.6.0'
h2DBVersion = '1.4.197'
}
11 changes: 11 additions & 0 deletions kales-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ sourceSets {
test.java.srcDirs += 'src/test/kotlin'
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}

mainClassName = 'kales.cli.CliKt'

project.ext.artifact = 'kales-cli'
Expand All @@ -21,5 +27,10 @@ dependencies {
implementation 'com.github.ajalt:clikt:1.6.0'
implementation 'com.squareup:kotlinpoet:1.0.1'
implementation project(":kales")
implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-script-runtime:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-script-util:$kotlinVersion"
testImplementation 'com.google.truth:truth:0.42'
testRuntime "com.h2database:h2:$h2DBVersion"
}
18 changes: 15 additions & 3 deletions kales-cli/src/main/kotlin/kales/cli/Cli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.arguments.multiple
import com.github.ajalt.clikt.parameters.types.file
import kales.cli.task.DbMigrateTask
import kales.cli.task.GenerateControllerTask
import kales.cli.task.NewCommandTask
import java.io.File

class Cli : CliktCommand() {
Expand All @@ -25,7 +28,7 @@ class New : CliktCommand(help = """
""".trimIndent())

override fun run() {
NewCommandRunner(appPath, appName).run()
NewCommandTask(appPath, appName).run()
}
}

Expand All @@ -36,6 +39,15 @@ class Generate : CliktCommand(help = """
override fun run() = Unit
}

class DbMigrate : CliktCommand(name = "db:migrate", help = """
Migrate the database
""".trimIndent()) {
override fun run() {
val workingDir = File(System.getProperty("user.dir"))
DbMigrateTask(workingDir).run()
}
}

class GenerateController : CliktCommand(name = "controller", help = """
Stubs out a new controller. Pass the CamelCased controller name.
Expand All @@ -47,13 +59,13 @@ class GenerateController : CliktCommand(name = "controller", help = """

override fun run() {
val workingDir = File(System.getProperty("user.dir"))
GenerateControllerCommandRunner(workingDir, name, actions.toSet()).run()
GenerateControllerTask(workingDir, name, actions.toSet()).run()
}
}

fun main(args: Array<String>) {
val generateCommand = Generate()
.subcommands(GenerateController())
Cli().subcommands(New(), generateCommand)
Cli().subcommands(New(), generateCommand, DbMigrate())
.main(args)
}
46 changes: 46 additions & 0 deletions kales-cli/src/main/kotlin/kales/cli/HarmonicaUp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package kales.cli

import com.improve_future.harmonica.core.AbstractMigration
import com.improve_future.harmonica.core.Connection
import com.improve_future.harmonica.service.VersionService
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
import java.io.File

internal class HarmonicaUp(
private val migrationsDirectory: File,
private val connection: Connection
) {
private val versionService = VersionService("schema_migrations")

fun run() {
connection.use { connection ->
connection.transaction {
versionService.setupHarmonicaMigrationTable(connection)
}
for (file in migrationsDirectory.listFiles().sortedBy { it.name }) {
val migrationVersion = file.name.split('_').first()
if (versionService.isVersionMigrated(connection, migrationVersion)) {
continue
}
connection.transaction {
val migration = readMigration(file.readText())
migration.connection = connection
migration.up()
versionService.saveVersion(connection, migrationVersion)
}
}
}
}

private fun readMigration(script: String) =
engine.eval(removePackageStatement(script)) as AbstractMigration

private companion object {
val engine by lazy {
KotlinJsr223JvmLocalScriptEngineFactory().scriptEngine
}

private fun removePackageStatement(script: String) =
script.replace(Regex("^\\s*package\\s+.+"), "")
}
}
51 changes: 51 additions & 0 deletions kales-cli/src/main/kotlin/kales/cli/PackageName.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package kales.cli

/** Represents a Java package, g.: com.foo.bar */
internal class PackageName private constructor(private val stringRepresentation: String) {
private val parts = stringRepresentation.split(".")

/** Returns a new [PackageName] representing the parent package */
val parentPackage by lazy {
if (parts.size > 1) {
PackageName(parts.slice(0..parts.size - 2).joinToString("."))
} else {
throw IllegalStateException("Unable to obtain parent package of '$this'")
}
}

fun isValid() = stringRepresentation.matches(VALID_PKG_REGEX.toRegex())

fun childPackage(vararg parts: String) =
PackageName("$stringRepresentation.${parts.joinToString(".")}")

override fun toString() = stringRepresentation

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as PackageName

if (stringRepresentation != other.stringRepresentation) return false

return true
}

override fun hashCode(): Int {
return stringRepresentation.hashCode()
}

companion object {
private val VALID_PKG_REGEX = "^(?:\\w+|\\w+\\.\\w+)+\$".toPattern()

/** Parses the provided [stringRepresentation] into a [PackageName] or throws if invalid */
fun parse(stringRepresentation: String): PackageName {
val pkg = PackageName(stringRepresentation)
return if (!pkg.isValid()) {
throw IllegalArgumentException("Invalid package name $stringRepresentation")
} else {
pkg
}
}
}
}
8 changes: 6 additions & 2 deletions kales-cli/src/main/kotlin/kales/cli/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fun File.safeListFiles(): List<File> {
}

/** Print messages about file changes when writing text to it */
fun File.safeWriteText(text: String, charset: Charset = Charsets.UTF_8) {
fun File.writeTextWithLogging(text: String, charset: Charset = Charsets.UTF_8) {
if (exists()) {
if (readText(charset) != text) {
printConflict()
Expand Down Expand Up @@ -49,7 +49,7 @@ fun File.printStatus(status: String) {
}

/** Copy streams and close at the end */
fun InputStream.safeCopyTo(destination: File): Long {
fun InputStream.copyToWithLogging(destination: File): Long {
if (!destination.exists() || destination.length() == 0L) {
destination.printCreated()
} else {
Expand All @@ -67,3 +67,7 @@ fun File.relativePathToWorkingDir(): String {
val workingDir = File(System.getProperty("user.dir"))
return workingDir.toPath().relativize(absoluteFile.toPath()).toString()
}

/** Returns a relative path String for a set of directory names */
fun relativePathFor(vararg pathSegments: String) =
pathSegments.toSet().joinToString(File.separator)
40 changes: 40 additions & 0 deletions kales-cli/src/main/kotlin/kales/cli/task/DbMigrateTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kales.cli.task

import com.github.ajalt.clikt.core.UsageError
import com.improve_future.harmonica.core.Connection
import com.improve_future.harmonica.core.DbConfig
import com.improve_future.harmonica.core.Dbms
import kales.cli.HarmonicaUp
import kales.cli.relativePathFor
import kales.migrations.KalesDatabaseConfig
import java.io.File

class DbMigrateTask(workingDirectory: File) : KalesContextualTask(workingDirectory) {
override fun run() {
val databaseYml = File(resourcesDir, "database.yml")
if (!databaseYml.exists()) {
throw UsageError("database.yml file not found.\n" +
"Plase make sure it exists under src/main/resources and try again")
}
val kalesDbConfig = KalesDatabaseConfig.fromDatabaseYml(databaseYml.inputStream())
val harmonicaDbConfig = DbConfig {
dbName = kalesDbConfig.database
user = kalesDbConfig.username
password = kalesDbConfig.password
host = kalesDbConfig.password
dbms = when (kalesDbConfig.adapter) {
"postgresql" -> Dbms.PostgreSQL
"mysql" -> Dbms.MySQL
"sqlite" -> Dbms.SQLite
"oracle" -> Dbms.Oracle
"sqlserver" -> Dbms.SQLServer
"h2" -> Dbms.H2
else -> throw IllegalArgumentException("Unknown database adapter ${kalesDbConfig.adapter}")
}
}
val dbMigrateDir = File(appDirectory.parentFile, relativePathFor("db", "migrate"))
val connection = Connection(harmonicaDbConfig)
val harmonicaUp = HarmonicaUp(dbMigrateDir, connection)
harmonicaUp.run()
}
}
Loading

0 comments on commit cc4b790

Please sign in to comment.