diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/devui/FlywayDevUIProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/devui/FlywayDevUIProcessor.java new file mode 100644 index 0000000000000..7bb9cbce4dab6 --- /dev/null +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/devui/FlywayDevUIProcessor.java @@ -0,0 +1,139 @@ +package io.quarkus.flyway.devui; + +import static java.util.List.of; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.quarkus.agroal.spi.JdbcInitialSQLGeneratorBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.dev.config.CurrentConfig; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.flyway.runtime.FlywayBuildTimeConfig; +import io.quarkus.flyway.runtime.FlywayDataSourceBuildTimeConfig; +import io.quarkus.flyway.runtime.devconsole.FlywayJsonRpcService; +import io.quarkus.runtime.configuration.ConfigUtils; + +public class FlywayDevUIProcessor { + + @BuildStep(onlyIf = IsDevelopment.class) + CardPageBuildItem create(FlywayBuildTimeConfig buildTimeConfig, + List generatorBuildItem, + CurateOutcomeBuildItem curateOutcomeBuildItem) { + + Map> initialSqlSuppliers = new HashMap<>(); + for (JdbcInitialSQLGeneratorBuildItem buildItem : generatorBuildItem) { + initialSqlSuppliers.put(buildItem.getDatabaseName(), buildItem.getSqlSupplier()); + } + + boolean isBaselineOnMigrateConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.baseline-on-migrate"); + boolean isMigrateAtStartConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.migrate-at-start"); + boolean isCleanAtStartConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.clean-at-start"); + String artifactId = curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getArtifactId(); + + DevConsoleManager.register("flyway-create-initial-migration", + createInitialMigration(buildTimeConfig, + initialSqlSuppliers, + artifactId, + isBaselineOnMigrateConfigured, + isMigrateAtStartConfigured, + isCleanAtStartConfigured)); + + CardPageBuildItem card = new CardPageBuildItem(); + + card.addPage(Page.webComponentPageBuilder() + .componentLink("qwc-flyway-datasources.js") + .dynamicLabelJsonRPCMethodName("getNumberOfDatasources") + .icon("font-awesome-solid:database")); + return card; + } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem registerJsonRpcBackend() { + return new JsonRPCProvidersBuildItem(FlywayJsonRpcService.class); + } + + private Function, String> createInitialMigration(FlywayBuildTimeConfig buildTimeConfig, + Map> initialSqlSuppliers, + String artifactId, + boolean isBaselineOnMigrateConfigured, + boolean isMigrateAtStartConfigured, + boolean isCleanAtStartConfigured) { + return (map -> { + String name = map.get("ds"); + if (name != null) { + try { + return createInitialMigrationScript(name, + buildTimeConfig, + artifactId, + initialSqlSuppliers, + isBaselineOnMigrateConfigured, + isMigrateAtStartConfigured, + isCleanAtStartConfigured); + } catch (Exception ex) { + return ex.getMessage(); + } + } + return "Datasource parameter not provided"; + }); + } + + private String createInitialMigrationScript(String name, + FlywayBuildTimeConfig buildTimeConfig, + String artifactId, + Map> initialSqlSuppliers, + boolean isBaselineOnMigrateConfigured, + boolean isMigrateAtStartConfigured, + boolean isCleanAtStartConfigured) throws Exception { + Supplier found = initialSqlSuppliers.get(name); + if (found == null) { + return "Error: Unable to find SQL generator"; + } + FlywayDataSourceBuildTimeConfig config = buildTimeConfig.getConfigForDataSourceName(name); + if (config.locations.isEmpty()) { + return "Error: Datasource has no locations configured"; + } + + List resourcesDir = DevConsoleManager.getHotReplacementContext().getResourcesDir(); + if (resourcesDir.isEmpty()) { + return "Error: No resource directory found"; + } + + // In the current project only + Path path = resourcesDir.get(0); + + Path migrationDir = path.resolve(config.locations.get(0)); + Files.createDirectories(migrationDir); + Path file = migrationDir.resolve( + "V1.0.0__" + artifactId + ".sql"); + Files.writeString(file, found.get()); + + Map newConfig = new HashMap<>(); + if (!isBaselineOnMigrateConfigured) { + newConfig.put("quarkus.flyway.baseline-on-migrate", "true"); + } + if (!isMigrateAtStartConfigured) { + newConfig.put("quarkus.flyway.migrate-at-start", "true"); + } + for (var profile : of("test", "dev")) { + if (!isCleanAtStartConfigured) { + newConfig.put("%" + profile + ".quarkus.flyway.clean-at-start", "true"); + } + } + CurrentConfig.EDITOR.accept(newConfig); + //force a scan, to make sure everything is up-to-date + DevConsoleManager.getHotReplacementContext().doScan(true); + + return "Initial migration created, Flyway will now manage this datasource"; + } +} diff --git a/extensions/flyway/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js b/extensions/flyway/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js new file mode 100644 index 0000000000000..fa43fceae024d --- /dev/null +++ b/extensions/flyway/deployment/src/main/resources/dev-ui/qwc-flyway-datasources.js @@ -0,0 +1,131 @@ +import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/text-field'; +import '@vaadin/text-area'; +import '@vaadin/form-layout'; +import '@vaadin/progress-bar'; +import '@vaadin/checkbox'; +import '@vaadin/grid'; +import 'qui-alert'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; +import '@vaadin/progress-bar'; +import { notifier } from 'notifier'; + +export class QwcFlywayDatasources extends QwcHotReloadElement { + + jsonRpc = new JsonRpc(this); + + static styles = css` + .button { + cursor: pointer; + } + .clearIcon { + color: var(--lumo-warning-text-color); + }`; + + static properties = { + _ds: {state: true} + } + + constructor() { + super(); + this._ds = null; + } + + connectedCallback() { + super.connectedCallback(); + this.hotReload(); + } + + hotReload(){ + this.jsonRpc.getDatasources().then(jsonRpcResponse => { + this._ds = jsonRpcResponse.result; + }); + } + + render() { + if (this._ds) { + return this._renderDataSourceTable(); + } else { + return html``; + } + } + + _renderDataSourceTable() { + return html` + + + + + + `; + } + + _actionRenderer(ds) { + return html`${this._renderMigrationButtons(ds)} + ${this._renderCreateButtons(ds)}`; + } + + _renderMigrationButtons(ds) { + if(ds.hasMigrations){ + return html` + this._clean(ds)} class="button"> + Clean + + this._migrate(ds)} class="button"> + Migrate + `; + } + } + + _renderCreateButtons(ds) { + console.log("ds -> " + JSON.stringify(ds)); + + if(ds.createPossible){ + return html` + this._create(ds)} class="button"> + Create Initial Migration File + `; + } + } + + _nameRenderer(ds) { + return html`${ds.name}`; + } + + _clean(ds) { + this.jsonRpc.clean({ds: ds.name}).then(jsonRpcResponse => { + this._showResultNotification(jsonRpcResponse.result); + }); + } + + _migrate(ds) { + this.jsonRpc.migrate({ds: ds.name}).then(jsonRpcResponse => { + this._showResultNotification(jsonRpcResponse.result); + }); + } + + _create(ds) { + this.jsonRpc.create({ds: ds.name}).then(jsonRpcResponse => { + this._showResultNotification(jsonRpcResponse.result); + }); + } + + _showResultNotification(response){ + if(response.type === "success"){ + notifier.showInfoMessage(response.message + " (" + response.number + ")"); + }else{ + notifier.showWarningMessage(response.message); + } + } + +} +customElements.define('qwc-flyway-datasources', QwcFlywayDatasources); \ No newline at end of file diff --git a/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devconsole/FlywayJsonRpcService.java b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devconsole/FlywayJsonRpcService.java new file mode 100644 index 0000000000000..66b5a45a2c8bf --- /dev/null +++ b/extensions/flyway/runtime/src/main/java/io/quarkus/flyway/runtime/devconsole/FlywayJsonRpcService.java @@ -0,0 +1,152 @@ +package io.quarkus.flyway.runtime.devconsole; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.output.CleanResult; +import org.flywaydb.core.api.output.MigrateResult; + +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.flyway.runtime.FlywayContainer; +import io.quarkus.flyway.runtime.FlywayContainersSupplier; + +public class FlywayJsonRpcService { + + public List getDatasources() { + List datasources = new ArrayList<>(); + Collection flywayContainers = new FlywayContainersSupplier().get(); + for (FlywayContainer fc : flywayContainers) { + datasources.add(new FlywayDatasource(fc.getDataSourceName(), fc.isHasMigrations(), fc.isCreatePossible())); + } + return datasources; + } + + public FlywayActionResponse clean(String ds) { + Flyway flyway = getFlyway(ds); + if (flyway != null) { + CleanResult cleanResult = flyway.clean(); + if (cleanResult.warnings != null && cleanResult.warnings.size() > 0) { + return new FlywayActionResponse("warning", + "Cleaning failed", + cleanResult.warnings.size(), + null, + cleanResult.database, cleanResult.warnings); + } else { + return new FlywayActionResponse("success", + "Cleaned", + cleanResult.schemasCleaned.size(), + null, + cleanResult.database); + } + + } + return errorNoDatasource(ds); + } + + public FlywayActionResponse migrate(String ds) { + Flyway flyway = getFlyway(ds); + if (flyway != null) { + MigrateResult migrateResult = flyway.migrate(); + if (migrateResult.success) { + return new FlywayActionResponse("success", + "Migration executed", + migrateResult.migrationsExecuted, + migrateResult.schemaName, + migrateResult.database); + } else { + return new FlywayActionResponse("warning", + "Migration failed", + migrateResult.warnings.size(), + migrateResult.schemaName, + migrateResult.database, + migrateResult.warnings); + } + } + return errorNoDatasource(ds); + } + + public FlywayActionResponse create(String ds) { + Flyway flyway = getFlyway(ds); + if (flyway != null) { + Map params = Map.of("ds", ds); + try { + String message = DevConsoleManager.invoke("flyway-create-initial-migration", params); + if (message.startsWith("Error: ")) { + return new FlywayActionResponse("error", message); + } else { + return new FlywayActionResponse("success", message); + } + } catch (Throwable t) { + new FlywayActionResponse("error", t.getMessage()); + } + } + return errorNoDatasource(ds); + } + + public int getNumberOfDatasources() { + Collection flywayContainers = new FlywayContainersSupplier().get(); + return flywayContainers.size(); + } + + private FlywayActionResponse errorNoDatasource(String ds) { + return new FlywayActionResponse("error", "Flyway datasource not found [" + ds + "]"); + } + + private Flyway getFlyway(String ds) { + Collection flywayContainers = new FlywayContainersSupplier().get(); + for (FlywayContainer flywayContainer : flywayContainers) { + if (flywayContainer.getDataSourceName().equals(ds)) { + return flywayContainer.getFlyway(); + } + } + return null; + } + + static class FlywayDatasource { + public String name; + public boolean hasMigrations; + public boolean createPossible; + + public FlywayDatasource() { + } + + public FlywayDatasource(String name, boolean hasMigrations, boolean createPossible) { + this.name = name; + this.hasMigrations = hasMigrations; + this.createPossible = createPossible; + } + } + + static class FlywayActionResponse { + public String type; + public String message; + public int number; + public String schema; + public String database; + public List warnings; + + public FlywayActionResponse() { + } + + public FlywayActionResponse(String type, String message) { + this(type, message, -1, null, null, List.of()); + } + + public FlywayActionResponse(String type, String message, int number, String schema, String database) { + this(type, message, number, schema, database, List.of()); + } + + public FlywayActionResponse(String type, String message, int number, String schema, String database, + List warnings) { + this.type = type; + this.message = message; + this.number = number; + this.schema = schema; + this.database = database; + this.warnings = warnings; + } + } +}