Skip to content

Commit

Permalink
Dev UI: Migrate Flyway extension
Browse files Browse the repository at this point in the history
Signed-off-by: Phillip Kruger <[email protected]>
  • Loading branch information
phillip-kruger committed Jul 28, 2023
1 parent b1626f4 commit a094358
Show file tree
Hide file tree
Showing 3 changed files with 422 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<JdbcInitialSQLGeneratorBuildItem> generatorBuildItem,
CurateOutcomeBuildItem curateOutcomeBuildItem) {

Map<String, Supplier<String>> 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<Map<String, String>, String> createInitialMigration(FlywayBuildTimeConfig buildTimeConfig,
Map<String, Supplier<String>> 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<String, Supplier<String>> initialSqlSuppliers,
boolean isBaselineOnMigrateConfigured,
boolean isMigrateAtStartConfigured,
boolean isCleanAtStartConfigured) throws Exception {
Supplier<String> 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<Path> 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<String, String> 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";
}
}
Original file line number Diff line number Diff line change
@@ -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`<vaadin-progress-bar class="progress" indeterminate></vaadin-progress-bar>`;
}
}

_renderDataSourceTable() {
return html`
<vaadin-grid .items="${this._ds}" class="datatable" theme="no-border">
<vaadin-grid-column auto-width
header="Name"
${columnBodyRenderer(this._nameRenderer, [])}>
</vaadin-grid-column>
<vaadin-grid-column auto-width
header="Action"
${columnBodyRenderer(this._actionRenderer, [])}
resizable>
</vaadin-grid-column>
</vaadin-grid>`;
}

_actionRenderer(ds) {
return html`${this._renderMigrationButtons(ds)}
${this._renderCreateButtons(ds)}`;
}

_renderMigrationButtons(ds) {
if(ds.hasMigrations){
return html`
<vaadin-button theme="small" @click=${() => this._clean(ds)} class="button">
<vaadin-icon class="clearIcon" icon="font-awesome-solid:broom"></vaadin-icon> Clean
</vaadin-button>
<vaadin-button theme="small" @click=${() => this._migrate(ds)} class="button">
<vaadin-icon icon="font-awesome-solid:arrow-right-arrow-left"></vaadin-icon> Migrate
</vaadin-button>`;
}
}

_renderCreateButtons(ds) {
console.log("ds -> " + JSON.stringify(ds));

if(ds.createPossible){
return html`
<vaadin-button theme="small" @click=${() => this._create(ds)} class="button">
<vaadin-icon icon="font-awesome-solid:arrow-right-arrow-left"></vaadin-icon> Create Initial Migration File
</vaadin-button>`;
}
}

_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);
Loading

0 comments on commit a094358

Please sign in to comment.