diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/CellTableCustomizer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/CellTableCustomizer.java index b427a66..f3b5a61 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/CellTableCustomizer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/CellTableCustomizer.java @@ -202,7 +202,7 @@ public class CellTableCustomizer extends WorkbenchTableCustomizer /******************************************************************************* ** Type, content and key checks. *******************************************************************************/ - private static void validate(SqlWorkbenchQBitConfig config, QRecord record, boolean inserting) + private static void validate(SqlWorkbenchQBitConfig config, QRecord record, boolean inserting) throws QException { if(inserting || record.getValues().containsKey("cellType")) { @@ -216,6 +216,7 @@ public class CellTableCustomizer extends WorkbenchTableCustomizer record.setValue("cellType", type); } + validateBackend(record, "backendName"); if(inserting && record.getValue("content") == null) { record.setValue("content", ""); diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/NotebookTableCustomizer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/NotebookTableCustomizer.java index 512c4f9..bac7b4c 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/NotebookTableCustomizer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/NotebookTableCustomizer.java @@ -262,7 +262,7 @@ public class NotebookTableCustomizer extends WorkbenchTableCustomizer /******************************************************************************* ** Title (required on insert, never blank) and parameters (a JSON list). *******************************************************************************/ - private static void validateContent(QRecord record, boolean inserting) + private static void validateContent(QRecord record, boolean inserting) throws QException { if(inserting || record.getValues().containsKey("title")) { @@ -277,6 +277,7 @@ public class NotebookTableCustomizer extends WorkbenchTableCustomizer } } + validateBackend(record, "defaultBackendName"); String parameters = record.getValueString("parametersJson"); if(StringUtils.hasContent(parameters)) { diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/WorkbenchTableCustomizer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/WorkbenchTableCustomizer.java index d1bc190..e91ba10 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/WorkbenchTableCustomizer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/customizers/WorkbenchTableCustomizer.java @@ -27,11 +27,14 @@ import java.util.Set; import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchRuntime; import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchSecurity; +import com.kingsrook.qbits.sqlworkbench.engine.WorkbenchBackends; import com.kingsrook.qbits.sqlworkbench.notebook.NotebookVersioning; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; @@ -278,4 +281,26 @@ public abstract class WorkbenchTableCustomizer implements TableCustomizerInterfa { return (SqlWorkbenchSecurity.isSystemUser()); } + + + + /******************************************************************************* + ** Optional backend selections must name an allowed workbench target. + *******************************************************************************/ + protected static void validateBackend(QRecord record, String fieldName) throws QException + { + String name = record.getValueString(fieldName); + if(name != null) + { + try + { + new WorkbenchBackends(QContext.getQInstance(), SqlWorkbenchRuntime.get()).getTarget(name); + } + catch(QNotFoundException e) + { + badInput(record, "Select an available SQL Workbench backend."); + } + } + } + } diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchSecurityAndJoinsMetaDataProducer.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchSecurityAndJoinsMetaDataProducer.java index 9e430b7..9dc8e1d 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchSecurityAndJoinsMetaDataProducer.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchSecurityAndJoinsMetaDataProducer.java @@ -19,11 +19,16 @@ package com.kingsrook.qbits.sqlworkbench.metadata; import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchQBitConfig; import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchSecurity; +import com.kingsrook.qbits.sqlworkbench.model.CellType; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitComponentMetaDataProducer; import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; @@ -49,6 +54,10 @@ public class SqlWorkbenchSecurityAndJoinsMetaDataProducer extends QBitComponentM output.add(keyType); } output.add(SqlWorkbenchTableMetaDataHelper.buildAudienceTypePossibleValueSource(config)); + output.add(QPossibleValueSource.newForEnum(config.applyPrefix(CellType.NAME), CellType.values())); + output.add(new QPossibleValueSource().withName(config.applyPrefix(WorkbenchBackendPossibleValueProvider.NAME)) + .withType(QPossibleValueSourceType.CUSTOM).withIdType(QFieldType.STRING) + .withCustomCodeReference(new QCodeReference(WorkbenchBackendPossibleValueProvider.class))); for(QJoinMetaData join : SqlWorkbenchTableMetaDataHelper.buildJoins(config)) { output.add(join); diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java index 72cac69..d962650 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/SqlWorkbenchTableMetaDataHelper.java @@ -27,6 +27,7 @@ import com.kingsrook.qbits.sqlworkbench.customizers.CellTableCustomizer; import com.kingsrook.qbits.sqlworkbench.customizers.NotebookTableCustomizer; import com.kingsrook.qbits.sqlworkbench.customizers.ShareTableCustomizer; import com.kingsrook.qbits.sqlworkbench.customizers.SnapshotTableCustomizer; +import com.kingsrook.qbits.sqlworkbench.model.CellType; import com.kingsrook.qbits.sqlworkbench.model.ShareAudienceType; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchCell; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchExecution; @@ -113,11 +114,12 @@ public class SqlWorkbenchTableMetaDataHelper .withSection(new QFieldSection("runner", "Runner", new QIcon("play_circle"), Tier.T2, null).withWidgetName(config.applyPrefix(SqlWorkbenchHomeWidgetsMetaDataProducer.NOTEBOOK_RUNNER))) .withSection(new QFieldSection("details", new QIcon("description"), Tier.T2, List.of("defaultBackendName", "parametersJson"))) .withSection(widgetSection(config, JOIN_NOTEBOOK_CELL, "Cells", "notes")) - .withSection(widgetSection(config, JOIN_NOTEBOOK_SHARE, "Shared With", "share")) - .withSection(widgetSection(config, JOIN_NOTEBOOK_SNAPSHOT, "Pinned Results", "push_pin")) + .withSection(widgetSection(config, JOIN_NOTEBOOK_SHARE, "Shares", "share")) + .withSection(widgetSection(config, JOIN_NOTEBOOK_SNAPSHOT, "Snapshots (pinned results)", "push_pin")) .withSection(widgetSection(config, JOIN_NOTEBOOK_VERSION, "Versions", "history")) .withSection(widgetSection(config, JOIN_NOTEBOOK_EXECUTION, "Runs", "policy")) .withSection(new QFieldSection("system", new QIcon("calendar_month"), Tier.T3, List.of("ownerUserId", "currentVersion", "createDate", "modifyDate"))); + table.getField("defaultBackendName").setPossibleValueSourceName(config.applyPrefix(WorkbenchBackendPossibleValueProvider.NAME)); table.getField("visibility").setIsEditable(false); table.getField("visibility").setIsRequired(false); table.getField("visibility").setDefaultValue("PERSONAL"); @@ -142,9 +144,12 @@ public class SqlWorkbenchTableMetaDataHelper .withSection(new QFieldSection("identity", new QIcon("badge"), Tier.T1, List.of("id", "notebookId", "sortOrder", "cellType", "title"))) .withSection(new QFieldSection("content", new QIcon("code"), Tier.T2, List.of("content", "backendName"))) .withSection(new QFieldSection("system", new QIcon("calendar_month"), Tier.T3, List.of("cellKey", "createDate", "modifyDate"))); + table.getField("backendName").setPossibleValueSourceName(config.applyPrefix(WorkbenchBackendPossibleValueProvider.NAME)); + table.getField("cellKey").setIsEditable(false); table.getField("cellKey").setIsRequired(false); + table.getField("cellType").setPossibleValueSourceName(config.applyPrefix(CellType.NAME)); table.getField("cellType").setIsRequired(false); - table.getField("cellType").setDefaultValue("SQL"); + table.getField("cellType").setDefaultValue(CellType.SQL.getPossibleValueId()); table.getField("content").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("sql"))); return (customize(linkNotebook(config, table), CellTableCustomizer.class)); } @@ -233,6 +238,8 @@ public class SqlWorkbenchTableMetaDataHelper .withSection(new QFieldSection("system", new QIcon("calendar_month"), Tier.T3, List.of("createDate"))); table.getField("sqlText").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("sql"))); table.getField("notebookId").setPossibleValueSourceName(config.applyPrefix(SqlWorkbenchNotebook.TABLE_NAME)); + table.getField("parametersJson").withIsEditable(false) + .withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json"))); appendOnly(table); return (table); } @@ -329,11 +336,11 @@ public class SqlWorkbenchTableMetaDataHelper .withOnlyIncludeFieldNames(List.of("id", "sortOrder", "cellType", "title", "backendName", "modifyDate")) .getWidgetMetaData()); widgets.add(ChildRecordListRenderer.widgetMetaDataBuilder(joins.get(config.applyPrefix(JOIN_NOTEBOOK_SHARE))) - .withLabel("Shared With").withCanAddChildRecord(true) + .withLabel("Shares").withCanAddChildRecord(true) .withOnlyIncludeFieldNames(List.of("id", "audienceType", "audienceId", "createdByName", "createDate")) .getWidgetMetaData()); widgets.add(ChildRecordListRenderer.widgetMetaDataBuilder(joins.get(config.applyPrefix(JOIN_NOTEBOOK_SNAPSHOT))) - .withLabel("Pinned Results") + .withLabel("Snapshots (pinned results)") .withOnlyIncludeFieldNames(List.of("id", "label", "cellKey", "rowCount", "truncated", "createDate")) .getWidgetMetaData()); widgets.add(ChildRecordListRenderer.widgetMetaDataBuilder(joins.get(config.applyPrefix(JOIN_NOTEBOOK_VERSION))) diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/WorkbenchBackendPossibleValueProvider.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/WorkbenchBackendPossibleValueProvider.java new file mode 100644 index 0000000..a57bae8 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/metadata/WorkbenchBackendPossibleValueProvider.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Kingsrook, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kingsrook.qbits.sqlworkbench.metadata; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchRuntime; +import com.kingsrook.qbits.sqlworkbench.engine.WorkbenchBackends; +import com.kingsrook.qqq.backend.core.actions.values.BasicCustomPossibleValueProvider; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue; + + +/******************************************************************************* + ** Backend choices use the same live allow/deny filtering as the Runner. + *******************************************************************************/ +public class WorkbenchBackendPossibleValueProvider extends BasicCustomPossibleValueProvider +{ + public static final String NAME = "sql_workbench_backend"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected QPossibleValue makePossibleValue(WorkbenchBackends.Target target) + { + return (new QPossibleValue<>(target.name(), target.name())); + } + + + + /******************************************************************************* + ** Unknown or excluded stored values have no selectable translation. + *******************************************************************************/ + @Override + protected WorkbenchBackends.Target getSourceObject(Serializable id) throws QException + { + return (getAllSourceObjects().stream().filter(target -> target.name().equals(id)).findFirst().orElse(null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected List getAllSourceObjects() throws QException + { + return (new WorkbenchBackends(QContext.getQInstance(), SqlWorkbenchRuntime.get()).listTargets()); + } +} diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/model/CellType.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/CellType.java new file mode 100644 index 0000000..c956120 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/model/CellType.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Kingsrook, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kingsrook.qbits.sqlworkbench.model; + + +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; + + +/******************************************************************************* + ** The supported notebook cell types. + *******************************************************************************/ +public enum CellType implements PossibleValueEnum +{ + SQL, + MARKDOWN; + + public static final String NAME = "sql_workbench_cell_type"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getPossibleValueId() + { + return (name()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String getPossibleValueLabel() + { + return (this == SQL ? "SQL" : "Markdown"); + } +} diff --git a/src/main/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookService.java b/src/main/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookService.java index 78e2ccd..0d55415 100644 --- a/src/main/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookService.java +++ b/src/main/java/com/kingsrook/qbits/sqlworkbench/notebook/NotebookService.java @@ -36,6 +36,7 @@ import com.kingsrook.qbits.sqlworkbench.engine.Caller; import com.kingsrook.qbits.sqlworkbench.engine.CollectedResult; import com.kingsrook.qbits.sqlworkbench.engine.ColumnInfo; import com.kingsrook.qbits.sqlworkbench.engine.ResultCache; +import com.kingsrook.qbits.sqlworkbench.model.CellType; import com.kingsrook.qbits.sqlworkbench.model.ShareAudienceType; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchCell; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebook; @@ -89,8 +90,8 @@ public class NotebookService { public static final String VISIBILITY_PERSONAL = "PERSONAL"; public static final String VISIBILITY_SHARED = "SHARED"; - public static final String CELL_SQL = "SQL"; - public static final String CELL_MARKDOWN = "MARKDOWN"; + public static final String CELL_SQL = CellType.SQL.getPossibleValueId(); + public static final String CELL_MARKDOWN = CellType.MARKDOWN.getPossibleValueId(); public static final int MAX_CELLS = 200; private final SqlWorkbenchRuntime runtime; diff --git a/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java b/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java index 9ae717d..8e083aa 100644 --- a/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java +++ b/src/test/java/com/kingsrook/qbits/sqlworkbench/SqlWorkbenchQBitProducerTest.java @@ -20,21 +20,13 @@ package com.kingsrook.qbits.sqlworkbench; import java.util.ArrayList; import java.util.List; import java.util.Map; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; -import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; -import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchAppMetaDataProducer; import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchHomeWidgetsMetaDataProducer; import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchRunnerAppMetaDataProducer; import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchWidgetMetaDataProducer; import com.kingsrook.qbits.sqlworkbench.metadata.SqlWorkbenchWidgetRenderer; +import com.kingsrook.qbits.sqlworkbench.metadata.WorkbenchBackendPossibleValueProvider; +import com.kingsrook.qbits.sqlworkbench.model.CellType; import com.kingsrook.qbits.sqlworkbench.model.ShareAudienceType; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchCell; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchExecution; @@ -42,6 +34,18 @@ import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebook; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebookShare; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchNotebookVersion; import com.kingsrook.qbits.sqlworkbench.model.SqlWorkbenchResultSnapshot; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import com.kingsrook.qqq.middleware.javalin.QJavalinMetaData; import org.junit.jupiter.api.Test; @@ -108,7 +112,8 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest QContext.clear(); assertThat(com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper.getAllAvailablePermissionNames(qInstance)) .contains("sql_workbench_notebook.read", "sql_workbench_notebook_share.insert", "sql_workbench.hasAccess") - .doesNotContain("sql_workbench_execution.delete", "sql_workbench_notebook_version.insert"); + .doesNotContain("sql_workbench_execution.insert", "sql_workbench_execution.edit", "sql_workbench_execution.delete", + "sql_workbench_notebook_version.insert", "sql_workbench_notebook_version.edit", "sql_workbench_result_snapshot.insert"); loginAs(READER); QWidgetMetaData widget = (QWidgetMetaData) qInstance.getWidget(SqlWorkbenchWidgetMetaDataProducer.NAME); @@ -155,6 +160,10 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest assertThat(instance.getTable("ns_sql_workbench_notebook")).isNotNull(); assertThat(instance.getTable("ns_sql_workbench_cell").getField("notebookId").getPossibleValueSourceName()).isEqualTo("ns_sql_workbench_notebook"); assertThat(instance.getApp("ns_sqlWorkbench")).isNotNull(); + assertThat(instance.getTable("ns_sql_workbench_cell").getField("backendName").getPossibleValueSourceName()).isEqualTo("ns_sql_workbench_backend"); + assertThat(instance.getPossibleValueSource("ns_sql_workbench_backend")).isNotNull(); + assertThat(instance.getTable("ns_sql_workbench_cell").getField("cellType").getPossibleValueSourceName()).isEqualTo("ns_sql_workbench_cell_type"); + assertThat(instance.getPossibleValueSource("ns_sql_workbench_cell_type")).isNotNull(); assertThat(QJavalinMetaData.of(instance)).isNull(); SqlWorkbenchRuntime first = prefixed.getRuntime(); @@ -227,6 +236,70 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest + /******************************************************************************* + ** Record screens expose readable sections and constrained editor fields. + *******************************************************************************/ + @Test + void testRecordScreenMetaData() throws Exception + { + QTableMetaData notebook = qInstance.getTable(SqlWorkbenchNotebook.TABLE_NAME); + QTableMetaData cell = qInstance.getTable(SqlWorkbenchCell.TABLE_NAME); + assertThat(notebook.getSections()).filteredOn(section -> section.getWidgetName() != null) + .extracting(section -> section.getLabel()) + .containsExactly("Runner", "Cells", "Shares", "Snapshots (pinned results)", "Versions", "Runs"); + for(var section : notebook.getSections()) + { + if(section.getWidgetName() != null && !"runner".equals(section.getName())) + { + assertThat(qInstance.getWidget(section.getWidgetName()).getLabel()).isEqualTo(section.getLabel()); + } + } + String backendPvs = WorkbenchBackendPossibleValueProvider.NAME; + assertThat(notebook.getField("defaultBackendName").getPossibleValueSourceName()).isEqualTo(backendPvs); + assertThat(cell.getField("backendName").getPossibleValueSourceName()).isEqualTo(backendPvs); + assertThat(qInstance.getPossibleValueSource(backendPvs).getType().name()).isEqualTo("CUSTOM"); + assertThat(cell.getField("cellKey").getIsEditable()).isFalse(); + assertThat(qInstance.getTable(SqlWorkbenchResultSnapshot.TABLE_NAME).getField("label").getIsEditable()).isTrue(); + assertThat(qInstance.getTable(SqlWorkbenchResultSnapshot.TABLE_NAME).getField("resultJson").getIsEditable()).isFalse(); + assertThat(cell.getField("cellType").getDefaultValue()).isEqualTo("SQL"); + assertThat(cell.getField("cellType").getPossibleValueSourceName()).isEqualTo(CellType.NAME); + assertThat(qInstance.getPossibleValueSource(CellType.NAME).getEnumValues()) + .extracting(value -> value.getId().toString()).containsExactly("SQL", "MARKDOWN"); + for(String table : List.of(SqlWorkbenchNotebook.TABLE_NAME, SqlWorkbenchExecution.TABLE_NAME)) + { + var field = qInstance.getTable(table).getField("parametersJson"); + assertThat(field.getAdornment(AdornmentType.CODE_EDITOR).orElseThrow().getValues()) + .containsEntry("languageMode", "json"); + assertThat(field.getIsEditable()).isEqualTo(table.equals(SqlWorkbenchNotebook.TABLE_NAME)); + } + } + + + + /******************************************************************************* + ** The live backend PVS honors all execution target exclusions. + *******************************************************************************/ + @Test + void testBackendChoices() throws Exception + { + qInstance.addBackend(defineH2Backend("other")); + qInstance.addBackend(defineH2Backend("excluded")); + qInstance.addBackend(defineH2Backend("h2__sqlWorkbenchReadOnly")); + qInstance.addBackend(defineH2Backend("h2__sqlWorkbenchWrite")); + qInstance.addBackend(new QBackendMetaData().withName("mem").withBackendType(MemoryBackendModule.class)); + config.setExcludedBackendNames(List.of("excluded")); + var provider = new WorkbenchBackendPossibleValueProvider(); + var input = new SearchPossibleValueSourceInput(); + assertThat(provider.search(input)).extracting(value -> value.getId()).containsExactlyInAnyOrder(BACKEND_NAME, "other"); + assertThat(provider.getPossibleValue(BACKEND_NAME).getLabel()).isEqualTo(BACKEND_NAME); + assertThat(provider.getPossibleValue("excluded")).isNull(); + assertThat(provider.getPossibleValue("unknown")).isNull(); + config.setAllowedBackendNames(List.of(BACKEND_NAME, "excluded")); + assertThat(provider.search(input)).extracting(value -> value.getId()).containsExactly(BACKEND_NAME); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/AppNavigationTest.java b/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/AppNavigationTest.java index aaa445f..fa04d02 100644 --- a/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/AppNavigationTest.java +++ b/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/AppNavigationTest.java @@ -118,7 +118,7 @@ class AppNavigationTest extends BaseSqlWorkbenchTest assertThat(qInstance.getJoin(SqlWorkbenchTableMetaDataHelper.JOIN_NOTEBOOK_EXECUTION).getRightTable()).isEqualTo(SqlWorkbenchExecution.TABLE_NAME); assertThat(qInstance.getWidget(SqlWorkbenchTableMetaDataHelper.JOIN_NOTEBOOK_EXECUTION).getLabel()).isEqualTo("Runs"); assertThat(notebook.getSections().stream().filter(s -> s.getWidgetName() != null).map(QFieldSection::getLabel).toList()) - .containsExactly("Runner", "Cells", "Shared With", "Pinned Results", "Versions", "Runs"); + .containsExactly("Runner", "Cells", "Shares", "Snapshots (pinned results)", "Versions", "Runs"); assertThat(notebook.getSections().get(0).getFieldNames()).containsExactly("id", "title", "description", "ownerName", "visibility"); } diff --git a/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/TableSecurityMatrixTest.java b/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/TableSecurityMatrixTest.java index 818be73..77e9b54 100644 --- a/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/TableSecurityMatrixTest.java +++ b/src/test/java/com/kingsrook/qbits/sqlworkbench/metadata/TableSecurityMatrixTest.java @@ -53,6 +53,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; @@ -86,7 +87,6 @@ import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryInput; import com.kingsrook.qqq.middleware.javalin.executors.io.TableQueryOutputInterface; import com.kingsrook.qqq.middleware.javalin.executors.io.TableUpdateInput; import com.kingsrook.qqq.middleware.javalin.executors.io.TableUpdateOutputInterface; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -991,4 +991,38 @@ class TableSecurityMatrixTest extends BaseSqlWorkbenchTest static final List ALL = List.of(SqlWorkbenchNotebook.TABLE_NAME, SqlWorkbenchCell.TABLE_NAME, SqlWorkbenchNotebookVersion.TABLE_NAME, SqlWorkbenchResultSnapshot.TABLE_NAME, SqlWorkbenchNotebookShare.TABLE_NAME, SqlWorkbenchExecution.TABLE_NAME); } + + /******************************************************************************* + ** Both insert and update paths reject backend text outside the live PVS. + *******************************************************************************/ + @Test + void testBackendSelectionValidation() throws Exception + { + login(OWNER); + Integer notebookId = ids.get("Private"); + for(String invalid : List.of("unknown", "", BACKEND_NAME)) + { + config.setExcludedBackendNames(invalid.equals(BACKEND_NAME) ? List.of(BACKEND_NAME) : List.of()); + QRecord notebook = new InsertAction().execute(new InsertInput(SqlWorkbenchNotebook.TABLE_NAME) + .withRecord(new QRecord().withValue("title", "Invalid backend").withValue("defaultBackendName", invalid))).getRecords().get(0); + assertThat(notebook.getErrors()).isNotEmpty(); + QRecord changed = new UpdateAction().execute(new UpdateInput(SqlWorkbenchNotebook.TABLE_NAME) + .withRecord(new QRecord().withValue("id", notebookId).withValue("defaultBackendName", invalid))).getRecords().get(0); + assertThat(changed.getErrors()).isNotEmpty(); + QRecord cell = new InsertAction().execute(new InsertInput(SqlWorkbenchCell.TABLE_NAME) + .withRecord(new QRecord().withValue("notebookId", notebookId).withValue("backendName", invalid))).getRecords().get(0); + assertThat(cell.getErrors()).isNotEmpty(); + Integer cellId = QueryAction.execute(SqlWorkbenchCell.TABLE_NAME, + new QQueryFilter(new QFilterCriteria("notebookId", QCriteriaOperator.EQUALS, notebookId))).get(0).getValueInteger("id"); + QRecord changedCell = new UpdateAction().execute(new UpdateInput(SqlWorkbenchCell.TABLE_NAME) + .withRecord(new QRecord().withValue("id", cellId).withValue("backendName", invalid))).getRecords().get(0); + assertThat(changedCell.getErrors()).isNotEmpty(); + } + config.setExcludedBackendNames(List.of()); + QRecord valid = new InsertAction().execute(new InsertInput(SqlWorkbenchCell.TABLE_NAME) + .withRecord(new QRecord().withValue("notebookId", notebookId).withValue("backendName", BACKEND_NAME))).getRecords().get(0); + assertThat(valid.getErrors()).isNullOrEmpty(); + assertThat(valid.getValueString("cellKey")).isNotBlank(); + } + }