This commit is contained in:
Maxime Duchene-Savard 2025-09-29 09:00:37 -04:00
parent 9ab8902dd6
commit 8d987ee50e
11 changed files with 258 additions and 256 deletions

7
.idea/kotlinc.xml generated
View File

@ -6,11 +6,4 @@
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.2" />
<option name="languageVersion" value="2.2" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.2.0" />
</component>
</project>

63
pom.xml
View File

@ -12,7 +12,6 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>2.2.0</kotlin.version>
</properties>
<build>
@ -25,42 +24,6 @@
<target>21</target>
</configuration>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>src/main/java</source>
<source>target/generated-sources/annotations</source>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>src/test/java</source>
<source>target/generated-test-sources/test-annotations</source>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@ -121,11 +84,18 @@
<version>1.8.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client -->
<!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.5.4</version>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
@ -165,16 +135,5 @@
<version>2.3.230</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,103 +1,20 @@
package dev.mduchene;
import io.javalin.Javalin;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.apache.commons.io.FileUtils;
import org.jooq.impl.DSL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;
/** Hello world! */
public class App {
public static void main(String[] args) {
System.setProperty("org.jooq.no-tips", "true");
System.setProperty("org.jooq.no-logo", "true");
Db db =
Db.Builder.create()
.url("jdbc:mariadb://localhost:3306/bolts")
Db.builder()
.url("jdbc:postgresql://127.0.0.1:5432/bolts")
.user("root")
.password("root")
.build();
db.init();
List<File> pendingBaseMigrations = Migration.of(db).getPendingBaseMigrations();
for (File file : pendingBaseMigrations) {
LoggerFactory.getLogger(App.class).info("Running migration: {}", file.getName());
try (Connection connection = db.getConnection()) {
String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
db.run(connection, content);
DSL.using(connection)
.insertInto(DSL.table("_migrations"))
.columns(DSL.field("name"), DSL.field("base"), DSL.field("executedAt"))
.values(
file.getName(), true, LocalDateTime.now()) // executedAt is null for base migrations
.execute();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
var app = Javalin.create(cnf -> {});
app.before(
ctx -> {
Connection connection = db.getConnection();
ctx.attribute("connection", connection);
});
app.after(
ctx -> {
Connection connection = ctx.attribute("connection");
if (connection != null) {
DbUtils.commitAndCloseQuietly(connection);
}
});
app.exception(
Exception.class,
(e, ctx) -> {
LoggerFactory.getLogger(App.class).error("error", e);
Connection connection = ctx.attribute("connection");
if (connection != null) {
DbUtils.rollbackAndCloseQuietly(connection);
}
ctx.status(500);
});
app.get(
"/",
ctx -> {
var r = new QueryRunner();
Integer query =
r.query(
(Connection) ctx.attribute("connection"),
"SELECT 1",
resultSet -> {
System.out.println("hello world");
if (resultSet.next()) return resultSet.getInt(1);
return null;
});
ctx.result(query.toString());
});
app.get(
"/error",
ctx -> {
throw new RuntimeException("error");
});
app.start(3001);
Migration.of(db).runBaseMigrations();
Server server = Server.builder().db(db).build();
server.start();
}
}

View File

@ -2,19 +2,12 @@ package dev.mduchene;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.jooq.DSLContext;
import org.jooq.impl.DSL;
import org.jooq.impl.SQLDataType;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import org.jooq.SQLDialect;
import org.jooq.conf.RenderQuotedNames;
import org.jooq.conf.Settings;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
public class Db {
private HikariDataSource dataSource;
@ -22,12 +15,9 @@ public class Db {
private Db() {}
public void init() {
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(true);
} catch (Exception e) {
// Ignore if the table already exists
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
@ -37,9 +27,6 @@ public class Db {
private Builder() {}
public static Builder create() {
return new Builder();
}
public Builder url(String url) {
this.url = url;

View File

@ -0,0 +1,37 @@
package dev.mduchene;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.http.HttpResponse;
import java.util.function.Supplier;
public class JsonBodyHandler implements HttpResponse.BodyHandler<JsonNode> {
private static JsonBodyHandler instance;
public static JsonBodyHandler getInstance() {
if (instance == null) {
instance = new JsonBodyHandler();
}
return instance;
}
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public HttpResponse.BodySubscriber<JsonNode> apply(HttpResponse.ResponseInfo responseInfo) {
HttpResponse.BodySubscriber<InputStream> upstream =
HttpResponse.BodySubscribers.ofInputStream();
return HttpResponse.BodySubscribers.mapping(upstream, this::toNode);
}
public JsonNode toNode(InputStream inputStream) {
try (InputStream stream = inputStream) {
return objectMapper.readTree(stream);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -1,14 +1,16 @@
package dev.mduchene;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.jooq.Meta;
import org.jooq.Record1;
import org.jooq.SelectConditionStep;
import org.jooq.impl.DSL;
import org.slf4j.LoggerFactory;
public class Migration {
@ -30,6 +32,25 @@ public class Migration {
.collect(Collectors.toList());
}
public void runBaseMigrations() {
List<File> pendingBaseMigrations = getPendingBaseMigrations();
for (File file : pendingBaseMigrations) {
LoggerFactory.getLogger(Migration.class).info("Running migration: {}", file.getName());
try (Connection connection = db.getConnection()) {
String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
db.run(connection, content);
DSL.using(connection)
.insertInto(DSL.table("_migrations"))
.columns(DSL.field("name"), DSL.field("base"), DSL.field("executedAt"))
.values(
file.getName(), true, LocalDateTime.now()) // executedAt is null for base migrations
.execute();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public boolean tableExists(String tableName) {
// Get the Meta object, which contains all database metadata
try {

View File

@ -0,0 +1,98 @@
package dev.mduchene;
import io.javalin.Javalin;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
public class Server {
private Javalin app;
private Db db;
public static Builder builder() {
return new Builder();
}
public Server start() {
app = Javalin.create(cnf -> {});
app.before(
ctx -> {
Connection connection = db.getConnection();
ctx.attribute("connection", connection);
});
app.after(
ctx -> {
Connection connection = ctx.attribute("connection");
if (connection != null) {
DbUtils.commitAndCloseQuietly(connection);
}
});
app.exception(
Exception.class,
(e, ctx) -> {
LoggerFactory.getLogger(App.class).error("error", e);
Connection connection = ctx.attribute("connection");
if (connection != null) {
DbUtils.rollbackAndCloseQuietly(connection);
}
ctx.status(500);
});
app.get(
"/",
ctx -> {
var r = new QueryRunner();
Integer query =
r.query(
(Connection) ctx.attribute("connection"),
"SELECT 1",
resultSet -> {
System.out.println("hello world");
if (resultSet.next()) return resultSet.getInt(1);
return null;
});
ctx.result(query.toString());
});
app.get(
"/error",
ctx -> {
throw new RuntimeException("error");
});
app.start(3001);
return this;
}
public Server stop() {
if (app != null) {
app.stop();
}
return this;
}
public static class Builder {
private Db db;
private Builder() {}
public Builder db(Db db) {
this.db = db;
return this;
}
public Server build() {
Server server = new Server();
server.db = this.db;
return server;
}
}
}

View File

@ -0,0 +1,12 @@
package dev.mduchene;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(BoltsIntegrationTest.class)
public class BasicHttpCallTest {
@Test
void test() {
get("http://localhost:7000/health");
}
}

View File

@ -0,0 +1,43 @@
package dev.mduchene;
import org.junit.jupiter.api.extension.*;
import java.sql.Connection;
public class BoltsIntegrationTest
implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback {
private Server server;
@Override
public void beforeAll(ExtensionContext context) throws Exception {
Db db =
Db.builder()
.url("jdbc:postgresql://127.0.0.1:5432/bolts_test")
.user("root")
.password("root")
.build();
try (Connection connection = db.getConnection()) {
connection.createStatement().execute("DROP SCHEMA IF EXISTS public CASCADE;");
connection.createStatement().execute("CREATE SCHEMA public;");
}
Migration.of(db).runBaseMigrations();
server = Server.builder().db(db).build().start();
}
@Override
public void afterAll(ExtensionContext context) throws Exception {
if (server != null) {
server.stop();
}
}
@Override
public void afterEach(ExtensionContext context) throws Exception {}
@Override
public void beforeEach(ExtensionContext context) throws Exception {}
}

View File

@ -1,96 +1,9 @@
package dev.mduchene;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(BoltsIntegrationTest.class)
public class MigrationTest {
// private Db db;
// private Migration migration;
//
// @BeforeEach
// public void setUp() {
// db = Db.Builder.create()
// .url("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MySQL")
// .user("sa")
// .password("")
// .build();
// db.init();
// migration = Migration.of(db);
// }
//
// @AfterEach
// public void tearDown() throws IOException {
// // Clean up the created migration files
// File migrationsDir = new File("src/main/resources/migrations");
// for (File file : Objects.requireNonNull(migrationsDir.listFiles())) {
// if (!file.getName().equals(".gitkeep")) {
// Files.delete(file.toPath());
// }
// }
// db.run("DROP ALL OBJECTS");
// }
//
//
//
// @Test
// public void testCreateMigration() {
// migration.create("test_migration");
// File migrationsDir = new File("src/main/resources/migrations");
// assertEquals(1, Objects.requireNonNull(migrationsDir.listFiles()).length);
// assertTrue(Objects.requireNonNull(migrationsDir.listFiles())[0].getName().contains("_test_migration.sql"));
// }
//
// @Test
// public void testGetPendingMigrations() throws IOException {
// // Create a dummy migration file
// Path newFile = Paths.get("src/main/resources/migrations/20230101000000_initial.sql");
// Files.createFile(newFile);
//
// List<String> pending = migration.getPending();
// assertEquals(1, pending.size());
// assertEquals("20230101000000_initial.sql", pending.get(0));
//
// // Run the migration
// db.run("INSERT INTO _migrations (name) VALUES ('20230101000000_initial.sql')");
//
// pending = migration.getPending();
// assertEquals(0, pending.size());
// }
//
// @Test
// public void testRunMigrations() throws IOException {
// // Create a dummy migration file with some SQL
// String migrationName = "20230101000001_create_users_table.sql";
// Path newFile = Paths.get("src/main/resources/migrations/" + migrationName);
// Files.write(newFile, "CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255));".getBytes());
//
// // Run pending migrations
// List<String> pending = migration.getPending();
// migration.run(pending);
//
// // Check if the table was created
// try {
// db.run("SELECT * FROM users");
// } catch (Exception e) {
// fail("Table 'users' should have been created by the migration.");
// }
//
// // Check if the migration was recorded in the _migrations table
//// List<String> ranMigrations = db.getRanMigrations();
//// assertEquals(1, ranMigrations.size());
//// assertEquals(migrationName, ranMigrations.get(0));
// }
}

View File

@ -0,0 +1,22 @@
package dev.mduchene;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class TestUtil {
public static HttpResponse<JsonNode> get(String url) {
UriBuilder.
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
try (var client = HttpClient.newHttpClient()) {
return client.send(request, JsonBodyHandler.getInstance());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}