diff --git a/README.md b/README.md index 8572786..d45101f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # iiko-connector +* `Числовые` → `Агрегация` +* `Категории` → `Группировка {ROW / COLUMN}` +* `Фильтры` → `Фильтрация` diff --git a/build.gradle.kts b/build.gradle.kts index 7a5b887..a07f5dc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,17 +48,26 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") - // https://mvnrepository.com/artifact/org.mindrot/jbcrypt + // Source: https://mvnrepository.com/artifact/org.mindrot/jbcrypt implementation("org.mindrot:jbcrypt:0.4") - // https://mvnrepository.com/artifact/org.slf4j/slf4j-api + // Source: https://mvnrepository.com/artifact/org.slf4j/slf4j-api implementation("org.slf4j:slf4j-api:2.0.17") - // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl + // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.25.4") - // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core + // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core implementation("org.apache.logging.log4j:log4j-core:2.25.4") - // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api + // Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api implementation("org.apache.logging.log4j:log4j-api:2.25.4") + implementation("io.vertx:vertx-jdbc-client") + + // Source: https://mvnrepository.com/artifact/com.clickhouse/clickhouse-jdbc + implementation("com.clickhouse:clickhouse-jdbc:0.9.8") + // Source: https://mvnrepository.com/artifact/com.mysql/mysql-connector-j + implementation("com.mysql:mysql-connector-j:9.7.0") + // Source: https://mvnrepository.com/artifact/org.postgresql/postgresql + implementation("org.postgresql:postgresql:42.7.11") + } java { diff --git a/frontend/src/components/Layout/AppLayout.vue b/frontend/src/components/Layout/AppLayout.vue index e66c7e3..b7e8310 100644 --- a/frontend/src/components/Layout/AppLayout.vue +++ b/frontend/src/components/Layout/AppLayout.vue @@ -93,6 +93,22 @@ {{ t('app.olapColumns') }} + + + + + {{ t('dbConnections.pageName') }} + + {{ t('app.settings') }} - - - - - - - - - - {{ t('app.database') }} - @@ -193,6 +185,17 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ t('common.id') }}{{ t('common.name') }}{{ t('dbConnections.type') }}{{ t('dbConnections.host') }}{{ t('dbConnections.port') }}{{ t('dbConnections.database') }}{{ t('dbConnections.user') }}{{ t('common.created') }}{{ t('common.actions') }}
{{ conn.id }}{{ conn.name }} + + {{ getTypeLabel(conn.type) }} + + {{ conn.host }}{{ conn.port }}{{ conn.database }}{{ conn.user }}{{ formatDate(conn.created) }} +
+ + + + + {{ conn.testResult }} + +
+
{{ t('dbConnections.noConnections') }}
+
+
+ + + +
+
+
+
+
+

{{ modalTitle }}

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +

{{ t('common.leavePasswordBlank') }}

+
+
+ + +
+
+
+
+
+
+ + + +
+
+
+
+
+
+ + + +
+

{{ t('dbConnections.delete') }}

+

{{ t('dbConnections.deleteConfirmation') }}

+
+ + +
+
+
+
+
+
+ + + + + + diff --git a/libs/RoaringBitmap-1.0.6.jar b/libs/RoaringBitmap-1.0.6.jar new file mode 100644 index 0000000..5c5f60c Binary files /dev/null and b/libs/RoaringBitmap-1.0.6.jar differ diff --git a/libs/antlr4-runtime-4.13.2.jar b/libs/antlr4-runtime-4.13.2.jar new file mode 100644 index 0000000..350c1d0 Binary files /dev/null and b/libs/antlr4-runtime-4.13.2.jar differ diff --git a/libs/asm-9.7.jar b/libs/asm-9.7.jar new file mode 100644 index 0000000..fee9b02 Binary files /dev/null and b/libs/asm-9.7.jar differ diff --git a/libs/clickhouse-client-0.9.8.jar b/libs/clickhouse-client-0.9.8.jar new file mode 100644 index 0000000..8d4fa82 Binary files /dev/null and b/libs/clickhouse-client-0.9.8.jar differ diff --git a/libs/clickhouse-data-0.9.8.jar b/libs/clickhouse-data-0.9.8.jar new file mode 100644 index 0000000..7eb3472 Binary files /dev/null and b/libs/clickhouse-data-0.9.8.jar differ diff --git a/libs/clickhouse-http-client-0.9.8.jar b/libs/clickhouse-http-client-0.9.8.jar new file mode 100644 index 0000000..d6a725d Binary files /dev/null and b/libs/clickhouse-http-client-0.9.8.jar differ diff --git a/libs/clickhouse-jdbc-0.9.8.jar b/libs/clickhouse-jdbc-0.9.8.jar new file mode 100644 index 0000000..8f3d6c4 Binary files /dev/null and b/libs/clickhouse-jdbc-0.9.8.jar differ diff --git a/libs/client-v2-0.9.8.jar b/libs/client-v2-0.9.8.jar new file mode 100644 index 0000000..d8bee18 Binary files /dev/null and b/libs/client-v2-0.9.8.jar differ diff --git a/libs/commons-codec-1.19.0.jar b/libs/commons-codec-1.19.0.jar new file mode 100644 index 0000000..ff6441a Binary files /dev/null and b/libs/commons-codec-1.19.0.jar differ diff --git a/libs/commons-compress-1.28.0.jar b/libs/commons-compress-1.28.0.jar new file mode 100644 index 0000000..ff7e6aa Binary files /dev/null and b/libs/commons-compress-1.28.0.jar differ diff --git a/libs/commons-io-2.20.0.jar b/libs/commons-io-2.20.0.jar new file mode 100644 index 0000000..5e06db2 Binary files /dev/null and b/libs/commons-io-2.20.0.jar differ diff --git a/libs/commons-lang3-3.20.0.jar b/libs/commons-lang3-3.20.0.jar new file mode 100644 index 0000000..8682b86 Binary files /dev/null and b/libs/commons-lang3-3.20.0.jar differ diff --git a/libs/error_prone_annotations-2.36.0.jar b/libs/error_prone_annotations-2.36.0.jar new file mode 100644 index 0000000..740268b Binary files /dev/null and b/libs/error_prone_annotations-2.36.0.jar differ diff --git a/libs/failureaccess-1.0.3.jar b/libs/failureaccess-1.0.3.jar new file mode 100644 index 0000000..2834ba1 Binary files /dev/null and b/libs/failureaccess-1.0.3.jar differ diff --git a/libs/guava-33.4.6-jre.jar b/libs/guava-33.4.6-jre.jar new file mode 100644 index 0000000..5e74385 Binary files /dev/null and b/libs/guava-33.4.6-jre.jar differ diff --git a/libs/httpclient5-5.4.4.jar b/libs/httpclient5-5.4.4.jar new file mode 100644 index 0000000..15fbd3d Binary files /dev/null and b/libs/httpclient5-5.4.4.jar differ diff --git a/libs/httpcore5-5.3.4.jar b/libs/httpcore5-5.3.4.jar new file mode 100644 index 0000000..44dff04 Binary files /dev/null and b/libs/httpcore5-5.3.4.jar differ diff --git a/libs/httpcore5-h2-5.3.4.jar b/libs/httpcore5-h2-5.3.4.jar new file mode 100644 index 0000000..03507de Binary files /dev/null and b/libs/httpcore5-h2-5.3.4.jar differ diff --git a/libs/j2objc-annotations-3.0.0.jar b/libs/j2objc-annotations-3.0.0.jar new file mode 100644 index 0000000..c293336 Binary files /dev/null and b/libs/j2objc-annotations-3.0.0.jar differ diff --git a/libs/jdbc-v2-0.9.8.jar b/libs/jdbc-v2-0.9.8.jar new file mode 100644 index 0000000..d763f86 Binary files /dev/null and b/libs/jdbc-v2-0.9.8.jar differ diff --git a/libs/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar b/libs/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar new file mode 100644 index 0000000..45832c0 Binary files /dev/null and b/libs/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar differ diff --git a/libs/lz4-java-1.10.4.jar b/libs/lz4-java-1.10.4.jar new file mode 100644 index 0000000..137537b Binary files /dev/null and b/libs/lz4-java-1.10.4.jar differ diff --git a/libs/mysql-connector-j-9.7.0.jar b/libs/mysql-connector-j-9.7.0.jar new file mode 100644 index 0000000..c2726da Binary files /dev/null and b/libs/mysql-connector-j-9.7.0.jar differ diff --git a/libs/postgresql-42.7.11.jar b/libs/postgresql-42.7.11.jar new file mode 100644 index 0000000..e42f175 Binary files /dev/null and b/libs/postgresql-42.7.11.jar differ diff --git a/libs/protobuf-java-4.31.1.jar b/libs/protobuf-java-4.31.1.jar new file mode 100644 index 0000000..f9c0c72 Binary files /dev/null and b/libs/protobuf-java-4.31.1.jar differ diff --git a/libs/vertx-jdbc-client-5.0.11.jar b/libs/vertx-jdbc-client-5.0.11.jar new file mode 100644 index 0000000..aa7461b Binary files /dev/null and b/libs/vertx-jdbc-client-5.0.11.jar differ diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java index 3d2b7c2..fa59af5 100644 --- a/src/main/java/su/xserver/iikocon/MainVerticle.java +++ b/src/main/java/su/xserver/iikocon/MainVerticle.java @@ -38,6 +38,7 @@ public class MainVerticle extends AbstractVerticle { private UserService userService; private RestaurantService restaurantService; + private ExternalDataBaseService externalDataBaseService; private SettingsService settingsService; @Override @@ -64,6 +65,7 @@ public class MainVerticle extends AbstractVerticle { userService = new UserService(db.getPool()); restaurantService = new RestaurantService(db.getPool()); settingsService = new SettingsService(db.getPool()); + externalDataBaseService = new ExternalDataBaseService(db.getPool(), vertx); userService.initDatabase().onFailure(err -> { log.error("Failed to initialize database", err); @@ -77,6 +79,10 @@ public class MainVerticle extends AbstractVerticle { log.error("Failed to initialize database", err); startPromise.fail(err); }); + externalDataBaseService.initDatabase().onFailure(err -> { + log.error("Failed to initialize database", err); + startPromise.fail(err); + }); createRouterAndStartHttp(startPromise); @@ -418,6 +424,8 @@ public class MainVerticle extends AbstractVerticle { .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); }); + externalDataBaseService.handleRoute(router); + new IikoHandler(vertx, router, db, restaurantService, authHandler); return router; diff --git a/src/main/java/su/xserver/iikocon/service/ExternalDataBaseService.java b/src/main/java/su/xserver/iikocon/service/ExternalDataBaseService.java new file mode 100644 index 0000000..0158a64 --- /dev/null +++ b/src/main/java/su/xserver/iikocon/service/ExternalDataBaseService.java @@ -0,0 +1,259 @@ +package su.xserver.iikocon.service; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.jdbcclient.JDBCConnectOptions; +import io.vertx.jdbcclient.JDBCPool; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PoolOptions; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.templates.SqlTemplate; +import su.xserver.iikocon.handler.AdminHandler; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class ExternalDataBaseService { + private final Pool pool; + private final Vertx vertx; + + public ExternalDataBaseService(Pool pool, Vertx vertx) { + this.pool = pool; + this.vertx = vertx; + } + + public void handleRoute(Router router) { + + router.get("/api/admin/database-connections").handler(rc -> this.getAllDataBases().onComplete(ar -> { + if (ar.succeeded()) { + rc.response() + .putHeader("Content-Type", "application/json") + .end(ar.result().encode()); + } else { + rc.response().setStatusCode(500).end(ar.cause().getMessage()); + } + })); + + router.get("/api/admin/database-connections/:id/test").handler(AdminHandler::requireAdmin).handler(rc -> { + int id = Integer.parseInt(rc.pathParam("id")); + + this.testConnection(id) + .onSuccess(result -> rc.response() + .setStatusCode(200) + .putHeader("Content-Type", "application/json") + .end(result.encode())) + .onFailure(err -> rc.response() + .setStatusCode(500) + .putHeader("Content-Type", "application/json") + .end(new JsonObject() + .put("success", false) + .put("error", err.getMessage()) + .encode())); + }); + + router.post("/api/admin/database-connections").handler(AdminHandler::requireAdmin).handler(rc -> { + JsonObject body = rc.body().asJsonObject(); + String name = body.getString("name"); + String type = body.getString("type"); + String host = body.getString("host"); + int port = body.getInteger("port"); + String database = body.getString("database"); + String user = body.getString("user"); + String password = body.getString("password"); + if (name == null || type == null || host == null || port < 1 || database == null || user == null || password == null) { + rc.response().setStatusCode(400).end("Missing fields"); + return; + } + this.createDataBase(name, type, host, port, database, user, password) + .onSuccess(v -> rc.response().setStatusCode(201).end()) + .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); + }); + + router.put("/api/admin/database-connections/:id").handler(AdminHandler::requireAdmin).handler(rc -> { + int id = Integer.parseInt(rc.pathParam("id")); + JsonObject body = rc.body().asJsonObject(); + String name = body.getString("name"); + String type = body.getString("type"); + String host = body.getString("host"); + int port = body.getInteger("port"); + String database = body.getString("database"); + String user = body.getString("user"); + String password = body.getString("password"); + if (name == null || type == null || host == null || port < 1 || database == null || user == null) { + rc.response().setStatusCode(400).end("Missing fields"); + return; + } + this.updateDataBase(id, name, type, host, port, database, user, password) + .onSuccess(v -> rc.response().end()) + .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); + }); + + router.delete("/api/admin/database-connections/:id").handler(AdminHandler::requireAdmin).handler(rc -> { + int id = Integer.parseInt(rc.pathParam("id")); + this.deleteDataBase(id) + .onSuccess(v -> rc.response().end()) + .onFailure(err -> rc.response().setStatusCode(500).end(err.getMessage())); + }); + } + + public Future initDatabase() { + String createTable = """ + CREATE TABLE IF NOT EXISTS external_database ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + type VARCHAR(40) UNIQUE NOT NULL, + host VARCHAR(255) NOT NULL, + port INT NOT NULL, + database VARCHAR(255) NOT NULL, + user VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + """; + return pool.query(createTable).execute().mapEmpty(); + } + + public Future createDataBase(String name, String type, String host, int port, String database, String user, String password) { + Map params = Map.of( + "name", name, + "type", type, + "host", host, + "port", port, + "database", database, + "user", user, + "password", password + ); + return SqlTemplate.forUpdate(pool, + "INSERT INTO external_database (name, type, host, port, database, user, password) VALUES (#{name}, #{type}, #{host}, #{port}, #{database}, #{user}, #{password})") + .execute(params) + .mapEmpty(); + } + + public Future getAllDataBases() { + return pool.query("SELECT id, name, type, host, port, database, user, password, created, updated FROM external_database ORDER BY id") + .execute() + .map(rows -> { + JsonArray array = new JsonArray(); + for (Row row : rows) { + array.add(new JsonObject() + .put("id", row.getInteger("id")) + .put("name", row.getString("name")) + .put("type", row.getString("type")) + .put("host", row.getString("host")) + .put("port", row.getInteger("port")) + .put("database", row.getString("database")) + .put("user", row.getString("user")) + .put("created", row.getLocalDateTime("created") != null ? + row.getLocalDateTime("created").toString() : null) + .put("updated", row.getLocalDateTime("updated") != null ? + row.getLocalDateTime("updated").toString() : null)); + } + return array; + }); + } + + public Future findById(int id) { + return SqlTemplate.forQuery(pool, + "SELECT id, name, type, host, port, database, user, password, created, updated FROM external_database WHERE id = #{id}") + .mapTo(row -> new JsonObject() + .put("id", row.getInteger("id")) + .put("name", row.getString("name")) + .put("type", row.getString("type")) + .put("host", row.getString("host")) + .put("port", row.getInteger("port")) + .put("database", row.getString("database")) + .put("user", row.getString("user")) + .put("password", row.getString("password")) + .put("created", row.getLocalDateTime("created") != null ? row.getLocalDateTime("created").toString() : null) + .put("updated", row.getLocalDateTime("updated") != null ? row.getLocalDateTime("updated").toString() : null)) + .execute(Collections.singletonMap("id", id)) + .map(rows -> rows.iterator().hasNext() ? rows.iterator().next() : null); + } + + public Future updateDataBase(int id, String name, String type, String host, int port, String database, String user, String password) { + Map params = new HashMap<>(); + params.put("id", id); + params.put("name", name); + params.put("type", type); + params.put("host", host); + params.put("port", port); + params.put("database", database); + params.put("user", user); + String sql; + if (password != null && !password.isEmpty()) { + params.put("password", password); + sql = "UPDATE external_database SET name = #{name}, type = #{type}, host = #{host}, port = #{port}, database = #{database}, user = #{user}, password = #{password} WHERE id = #{id}"; + } else { + sql = "UPDATE external_database SET name = #{name}, type = #{type}, host = #{host}, port = #{port}, database = #{database}, user = #{user} WHERE id = #{id}"; + } + return SqlTemplate.forUpdate(pool, sql).execute(params).mapEmpty(); + } + + public Future deleteDataBase(int id) { + return SqlTemplate.forUpdate(pool, "DELETE FROM external_database WHERE id = #{id}") + .execute(Collections.singletonMap("id", id)) + .mapEmpty(); + } + + public Future testConnection(int id) { + Promise promise = Promise.promise(); + + this.findById(id) + .onSuccess(conn -> { + String jdbcUrl = buildJdbcUrl(conn); + if (jdbcUrl == null) { + promise.fail("Unsupported database type: " + conn.getString("type")); + return; + } + + JDBCConnectOptions connectOptions = new JDBCConnectOptions() + .setJdbcUrl(jdbcUrl) + .setDatabase(conn.getString("database")) + .setUser(conn.getString("user")) + .setPassword(conn.getString("password")); + + PoolOptions poolOptions = new PoolOptions() + .setMaxSize(1); + + Pool pool = JDBCPool.pool(vertx, connectOptions, poolOptions); + + long startTime = System.currentTimeMillis(); + + pool + .query("SELECT 1") + .execute() + .onSuccess(rows -> { + long latency = System.currentTimeMillis() - startTime; + JsonObject result = new JsonObject() + .put("success", true) + .put("latency_ms", latency); + promise.complete(result); + pool.close(); + }) + .onFailure(err -> promise.fail("Connection failed: " + err.getMessage())); + }) + .onFailure(promise::fail); + + return promise.future(); + } + + private String buildJdbcUrl(JsonObject conn) { + return switch (conn.getString("type").toLowerCase()) { + case "mysql" -> String.format("jdbc:mysql://%s:%d", + conn.getString("host"), conn.getInteger("port")); + case "postgres" -> String.format("jdbc:postgresql://%s:%d", + conn.getString("host"), conn.getInteger("port")); + case "clickhouse" -> + String.format("jdbc:clickhouse://%s:%d", + conn.getString("host"), conn.getInteger("port")); + default -> null; + }; + } + +} diff --git a/src/main/java/su/xserver/iikocon/test/ClickHouseJDBCExample.java b/src/main/java/su/xserver/iikocon/test/ClickHouseJDBCExample.java new file mode 100644 index 0000000..4a6af6b --- /dev/null +++ b/src/main/java/su/xserver/iikocon/test/ClickHouseJDBCExample.java @@ -0,0 +1,41 @@ +package su.xserver.iikocon.test; + +import io.vertx.core.Vertx; +import io.vertx.jdbcclient.JDBCConnectOptions; +import io.vertx.jdbcclient.JDBCPool; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PoolOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ClickHouseJDBCExample { + private static final Logger log = LoggerFactory.getLogger(ClickHouseJDBCExample.class); + + public static void main(String[] args) { + Vertx vertx = Vertx.vertx(); + + JDBCConnectOptions connectOptions = new JDBCConnectOptions() + .setJdbcUrl("jdbc:clickhouse://dl-import.aramagedec.ru:8123") + .setDatabase("test") + .setUser("clickhouse_admin") + .setPassword("7002ITinsta11"); + + PoolOptions poolOptions = new PoolOptions() + .setMaxSize(16); + + Pool pool = JDBCPool.pool(vertx, connectOptions, poolOptions); + + pool + .query("SELECT 1") + .execute() + .onSuccess(rows -> { + rows.forEach(row -> log.info(row.toJson().encodePrettily())); + vertx.close(); + }) + .onFailure(err -> { + log.error(err.getMessage()); + vertx.close(); + }); + } + +}