Compare commits

..
Author SHA1 Message Date
james.maesandClaude Opus 5.5 b0b027a9aa docs: name 1.0.0-RC.4 as the snake_case rename release
1.0.0-RC.3 on Maven Central is an accidental republish of RC.2, so the
rename shipped in 1.0.0-RC.4. Point UPGRADE.md, the @Deprecated(since)
annotations, their javadoc, the RC.4 changelog wording, the README
upgrade row and the naming test comment at RC.4, and note that upgrading
from RC.3 is the same as upgrading from RC.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:05:36 -05:00
james.maes 4d5139cab6 Merge pull request #16 from QRun-IO/feature/snapshot-insert-permission
fix(permissions): grant result_snapshot.insert to users (pinning under core enforcement)
2026-10-12 16:25:08 -05:00
james.maes 3ca94f7da5 Merge branch 'develop' into feature/snapshot-insert-permission 2026-10-12 16:23:04 -05:00
james.maes cfb176e3ac Merge pull request #17 from QRun-IO/feature/note-burned-rc
docs: note that 1.0.0-RC.3 is a burned republish of RC.2
2026-10-12 16:17:20 -05:00
james.maesandClaude Opus 5.5 c858bcb720 docs: note that 1.0.0-RC.3 is a burned republish of RC.2
A tag build of v1.0.0-RC.2 published 1.0.0-RC.3 with the RC.2 code
(QRun-IO/qqq-orb#20). Add a dated do-not-use entry for RC.3 and move
the unreleased work to 1.0.0-RC.4, the next real candidate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:13:36 -05:00
james.maesandClaude Opus 5.5 e156a710e3 fix(permissions): grant result_snapshot.insert to users (pinning under core enforcement)
Pinning a result inserts a sql_workbench_result_snapshot row as a top-level
action. With QQQ core permission enforcement on, that insert is checked
against the session's permissions, and getUserPermissionNames() granted
only read/edit/delete on snapshots, so every user's pin was denied and
hosts had to grant the insert by hand. Record locks still limit snapshots
to the user's own notebooks.

New test: with enforcement on, the user permission set covers create,
update, run, pin, unpin and delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:12:25 -05:00
james.maes 9ca854e59a Merge pull request #14 from QRun-IO/feature/record-screen-polish
fix(screens): plain child-list labels, backend and cell-type pickers, read-only keys and JSON
2026-10-12 16:08:17 -05:00
james.maes 6029174344 Merge branch 'develop' into feature/record-screen-polish 2026-10-12 16:06:11 -05:00
james.maes 1ffc663ba5 Merge pull request #6 from QRun-IO/feature/developer-panel
feat: SQL Workbench record developer panel
2026-10-12 16:05:24 -05:00
james.maesandClaude Opus 5.5 dd4bd63b2b fix(screens): plain child-list labels, backend and cell-type pickers, read-only keys and JSON
From the kof22 record-screen audit:
- notebook child lists are labelled Cells, Shares, Snapshots and Versions
  instead of raw join names
- notebook defaultBackendName and cell backendName use a possible-value source
  of the backends the workbench can target (invalid values are rejected)
- cell cellType uses a SQL/MARKDOWN enum possible-value source
- cell cellKey is read-only
- execution parametersJson shows as read-only JSON

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:53:17 -05:00
23 changed files with 371 additions and 58 deletions

No files matched your search

+11 -4
View File
@@ -2,7 +2,7 @@
All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow semantic versioning.
## [1.0.0-RC.3] - Unreleased
## [1.0.0-RC.4] - Unreleased
### Added
- **App home dashboard.** The `sqlWorkbench` app's home is now an information dashboard of real data, read in the viewer's session (permissions and record locks apply): summary tiles and links, runs per day, recent notebooks, recent runs, slowest and failed queries, most-used notebooks, shared notebooks, committed writes, and (workbench admins only) activity by user. Each widget has an honest empty state.
@@ -17,12 +17,19 @@ All notable changes to this project are documented here. The format follows Keep
- **Names follow the owner rule: lowercase snake_case, prefixed with the qbit's name (metadata only, no DDL).** Tables `sql_workbench_notebook`, `sql_workbench_cell`, `sql_workbench_notebook_version`, `sql_workbench_result_snapshot`, `sql_workbench_notebook_share`, `sql_workbench_execution` (they now equal their backend table names); possible-value source `sql_workbench_share_audience_type`; permissions `sql_workbench.*`, `sql_workbench_admin.hasAccess` and `<table>.read` etc.; security key types `sql_workbench_user` (all-access `sql_workbench_all_access`) and `sql_workbench_audience`; joins `sql_workbench_notebook_join_sql_workbench_*`.
- `tableNamePrefix` (and the QBit namespace) is normalized to lowercase snake_case and joined with one `_`.
### Deprecated
- `LEGACY_TABLE_NAME` on each entity, `ShareAudienceType.LEGACY_NAME` and `SqlWorkbenchQBitConfig.LEGACY_*` hold the pre-RC.3 names, for migrating stored references only.
### Fixed
- **Pinning a result works with QQQ core permission enforcement on.** `getUserPermissionNames()` now includes `sql_workbench_result_snapshot.insert`: pinning is a top-level insert, which core enforcement checks, so hosts no longer need to grant it by hand. Record locks still limit snapshots to the user's own notebooks.
### Upgrade from RC.2
### Deprecated
- `LEGACY_TABLE_NAME` on each entity, `ShareAudienceType.LEGACY_NAME` and `SqlWorkbenchQBitConfig.LEGACY_*` hold the pre-RC.4 names, for migrating stored references only.
### Upgrade from RC.2 (or RC.3, a copy of RC.2)
- Follow [UPGRADE.md](UPGRADE.md): grant the renamed permissions and rename stored table names (audit, saved views and reports). No DDL.
## [1.0.0-RC.3] - 2026-10-12
**Do not use.** 1.0.0-RC.3 on Maven Central is an accidental republish of the RC.2 code by a tag build; do not use it. The next real candidate is 1.0.0-RC.4. (Cause: QRun-IO/qqq-orb#20; the `publish_release` workflow now runs on GA tags only.)
## [1.0.0-RC.2] - Unreleased
The workbench's data becomes standard QQQ tables, secured by QQQ's own mechanisms instead of being hidden ([#3](https://github.com/QRun-IO/qbit-sql-workbench/issues/3)). Requires QQQ `4.1.0-RC.4` or later ([qqq#1073](https://github.com/QRun-IO/qqq/issues/1073)).
+2 -2
View File
@@ -114,7 +114,7 @@ Everything is denied by default. Two standard QQQ layers apply:
| Role | Grant | What it allows |
|---|---|---|
| User | `config.getUserPermissionNames()`: `sql_workbench.hasAccess`, plus read/insert/edit/delete on notebooks and cells, read on versions, shares and the audit, and read/edit/delete on pinned results | The app, widget and endpoints (read-only SQL, EXPLAIN, schema). Their own notebooks, read-only access to notebooks shared with them or in the library, and their own execution history. |
| User | `config.getUserPermissionNames()`: `sql_workbench.hasAccess`, plus read/insert/edit/delete on notebooks and cells, read on versions, shares and the audit, and read/insert/edit/delete on pinned results | The app, widget and endpoints (read-only SQL, EXPLAIN, schema). Their own notebooks, read-only access to notebooks shared with them or in the library, and their own execution history. |
| Writer | `config.getWritePermissionName()` (`sql_workbench.write`) | Write mode (preview and commit) and EXPLAIN ANALYZE. |
| Sharer | `config.getSharePermissionNames()`: `sql_workbench.share`, plus insert/edit/delete on shares | Sharing their own notebooks with users, groups, or everyone (publishing to the library). |
| Admin | `config.getAdminPermissionNames()`: the user permissions plus `sql_workbench_admin.hasAccess` | The all-access key: every user's notebooks, cells, versions, pinned results (real query data), shares, and the whole execution audit, and editing any notebook. |
@@ -244,7 +244,7 @@ A checklist for adding the workbench to a QQQ application. Each row points to th
| ESB and search | Optional: wire `SqlWorkbenchIntegrations.esbTableNames` and `quickSearchTables` into your ESB and Quick Search config ([ESB and Quick Search](#esb-and-quick-search)). |
| Turning writes off | `withWriteModeEnabled(false)` disables write mode for everyone. |
| Known limitations | Raw SQL bypasses QQQ security, audits, listeners and automations ([Safety model](#safety-model)). H2 and generic backends have no enforced read-only transaction. Only PostgreSQL DDL ships. Shares are read-only (no shared editing). |
| Upgrading | From RC.2: the QQQ names became snake_case (`sql_workbench_*`; no DDL). See [UPGRADE.md](UPGRADE.md) for permissions and stored table names. From RC.1: also run the share-table DDL and backfill, grant the new permission lists (the tables no longer rely on the admin `hasAccess` permission alone), call `applyToSession`, and use QQQ `4.1.0-RC.4` or later. |
| Upgrading | From RC.2 (or RC.3, a copy of RC.2): the QQQ names became snake_case (`sql_workbench_*`; no DDL). See [UPGRADE.md](UPGRADE.md) for permissions and stored table names. From RC.1: also run the share-table DDL and backfill, grant the new permission lists (the tables no longer rely on the admin `hasAccess` permission alone), call `applyToSession`, and use QQQ `4.1.0-RC.4` or later. |
Nothing in the qbit is specific to one host application.
+5 -3
View File
@@ -1,10 +1,12 @@
# Upgrading
## To 1.0.0-RC.3: snake_case names (metadata only)
## To 1.0.0-RC.4: snake_case names (metadata only)
The owner naming rule now applies to every QRun-IO qbit: table names are lowercase snake_case, prefixed with the qbit's name. In 1.0.0-RC.3 the SQL Workbench's QQQ names change to match. **The database does not change**: the physical tables were already `sql_workbench_*` (`docs/ddl/postgres.sql`), so no DDL runs. What changes is the names QQQ uses, and anything your app stored under the old names.
The rename shipped in 1.0.0-RC.4. 1.0.0-RC.3 on Maven Central is an accidental republish of the RC.2 code (see CHANGELOG.md), so upgrading from RC.3 is the same as upgrading from RC.2.
| Kind | Before (RC.1, RC.2) | From RC.3 |
The owner naming rule now applies to every QRun-IO qbit: table names are lowercase snake_case, prefixed with the qbit's name. In 1.0.0-RC.4 the SQL Workbench's QQQ names change to match. **The database does not change**: the physical tables were already `sql_workbench_*` (`docs/ddl/postgres.sql`), so no DDL runs. What changes is the names QQQ uses, and anything your app stored under the old names.
| Kind | Before (RC.1, RC.2, RC.3) | From RC.4 |
|---|---|---|
| Tables (and the notebook possible-value source) | `sqlWorkbenchNotebook`, `sqlWorkbenchCell`, `sqlWorkbenchNotebookVersion`, `sqlWorkbenchResultSnapshot`, `sqlWorkbenchNotebookShare`, `sqlWorkbenchExecution` | `sql_workbench_notebook`, `sql_workbench_cell`, `sql_workbench_notebook_version`, `sql_workbench_result_snapshot`, `sql_workbench_notebook_share`, `sql_workbench_execution` |
| Enum possible-value source | `sqlWorkbenchShareAudienceType` | `sql_workbench_share_audience_type` |
@@ -71,23 +71,23 @@ public class SqlWorkbenchQBitConfig implements QBitConfig
public static final String ALL_ACCESS_SECURITY_KEY_NAME = "sql_workbench_all_access";
///////////////////////////////////////////////////////////////////////////
// names before 1.0.0-RC.3, when the owner rule (lowercase snake_case, //
// names before 1.0.0-RC.4, when the owner rule (lowercase snake_case, //
// prefixed with the qbit name) was applied; kept only so hosts can //
// migrate stored references (see UPGRADE.md) //
///////////////////////////////////////////////////////////////////////////
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_ACCESS_PERMISSION_BASE_NAME = "sqlWorkbench";
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_WRITE_PERMISSION_NAME = "sqlWorkbench.write";
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_SHARE_PERMISSION_NAME = "sqlWorkbench.share";
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_ADMIN_PERMISSION_BASE_NAME = "sqlWorkbenchAdmin";
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_USER_SECURITY_KEY_TYPE_NAME = "sqlWorkbenchUser";
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_AUDIENCE_SECURITY_KEY_TYPE_NAME = "sqlWorkbenchAudience";
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_ALL_ACCESS_SECURITY_KEY_NAME = "sqlWorkbenchAllAccess";
private String backendName;
@@ -377,8 +377,8 @@ public class SqlWorkbenchQBitConfig implements QBitConfig
/*******************************************************************************
** Every permission an ordinary workbench user needs: the access
** permission, read / insert / edit / delete on notebooks and cells, read
** on versions, shares and the execution audit, and read / edit (label) /
** delete on pinned snapshots. Record locks narrow every one of these to
** on versions, shares and the execution audit, and read / insert (pin) /
** edit (label) / delete on pinned snapshots. Record locks narrow every one of these to
** the user's own (or shared-with-them, for reads) rows.
*******************************************************************************/
@JsonIgnore
@@ -389,7 +389,7 @@ public class SqlWorkbenchQBitConfig implements QBitConfig
addTablePermissions(names, SqlWorkbenchNotebook.TABLE_NAME, "read", "insert", "edit", "delete");
addTablePermissions(names, SqlWorkbenchCell.TABLE_NAME, "read", "insert", "edit", "delete");
addTablePermissions(names, SqlWorkbenchNotebookVersion.TABLE_NAME, "read");
addTablePermissions(names, SqlWorkbenchResultSnapshot.TABLE_NAME, "read", "edit", "delete");
addTablePermissions(names, SqlWorkbenchResultSnapshot.TABLE_NAME, "read", "insert", "edit", "delete");
addTablePermissions(names, SqlWorkbenchNotebookShare.TABLE_NAME, "read");
addTablePermissions(names, SqlWorkbenchExecution.TABLE_NAME, "read");
return (names);
@@ -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", "");
@@ -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))
{
@@ -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.");
}
}
}
}
@@ -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);
@@ -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)))
@@ -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<WorkbenchBackends.Target, String>
{
public static final String NAME = "sql_workbench_backend";
/*******************************************************************************
**
*******************************************************************************/
@Override
protected QPossibleValue<String> 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<WorkbenchBackends.Target> getAllSourceObjects() throws QException
{
return (new WorkbenchBackends(QContext.getQInstance(), SqlWorkbenchRuntime.get()).listTargets());
}
}
@@ -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<String>
{
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");
}
}
@@ -33,9 +33,9 @@ public enum ShareAudienceType implements PossibleValueEnum<String>
public static final String NAME = "sql_workbench_share_audience_type";
/*******************************************************************************
** The possible-value source name before 1.0.0-RC.3.
** The possible-value source name before 1.0.0-RC.4.
*******************************************************************************/
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_NAME = "sqlWorkbenchShareAudienceType";
private final String label;
@@ -32,10 +32,10 @@ public class SqlWorkbenchCell extends QRecordEntity
public static final String TABLE_NAME = "sql_workbench_cell";
/*******************************************************************************
** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case,
** The table name before 1.0.0-RC.4 (owner rule: lowercase snake_case,
** prefixed with the qbit name). Kept for migrating stored references only.
*******************************************************************************/
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_TABLE_NAME = "sqlWorkbenchCell";
@QField(isEditable = false, isPrimaryKey = true)
@@ -34,10 +34,10 @@ public class SqlWorkbenchExecution extends QRecordEntity
public static final String TABLE_NAME = "sql_workbench_execution";
/*******************************************************************************
** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case,
** The table name before 1.0.0-RC.4 (owner rule: lowercase snake_case,
** prefixed with the qbit name). Kept for migrating stored references only.
*******************************************************************************/
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_TABLE_NAME = "sqlWorkbenchExecution";
@QField(isEditable = false, isPrimaryKey = true)
@@ -33,10 +33,10 @@ public class SqlWorkbenchNotebook extends QRecordEntity
public static final String TABLE_NAME = "sql_workbench_notebook";
/*******************************************************************************
** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case,
** The table name before 1.0.0-RC.4 (owner rule: lowercase snake_case,
** prefixed with the qbit name). Kept for migrating stored references only.
*******************************************************************************/
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_TABLE_NAME = "sqlWorkbenchNotebook";
@QField(isEditable = false, isPrimaryKey = true)
@@ -36,10 +36,10 @@ public class SqlWorkbenchNotebookShare extends QRecordEntity
public static final String TABLE_NAME = "sql_workbench_notebook_share";
/*******************************************************************************
** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case,
** The table name before 1.0.0-RC.4 (owner rule: lowercase snake_case,
** prefixed with the qbit name). Kept for migrating stored references only.
*******************************************************************************/
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_TABLE_NAME = "sqlWorkbenchNotebookShare";
@QField(isEditable = false, isPrimaryKey = true)
@@ -32,10 +32,10 @@ public class SqlWorkbenchNotebookVersion extends QRecordEntity
public static final String TABLE_NAME = "sql_workbench_notebook_version";
/*******************************************************************************
** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case,
** The table name before 1.0.0-RC.4 (owner rule: lowercase snake_case,
** prefixed with the qbit name). Kept for migrating stored references only.
*******************************************************************************/
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_TABLE_NAME = "sqlWorkbenchNotebookVersion";
@QField(isEditable = false, isPrimaryKey = true)
@@ -33,10 +33,10 @@ public class SqlWorkbenchResultSnapshot extends QRecordEntity
public static final String TABLE_NAME = "sql_workbench_result_snapshot";
/*******************************************************************************
** The table name before 1.0.0-RC.3 (owner rule: lowercase snake_case,
** The table name before 1.0.0-RC.4 (owner rule: lowercase snake_case,
** prefixed with the qbit name). Kept for migrating stored references only.
*******************************************************************************/
@Deprecated(since = "1.0.0-RC.3")
@Deprecated(since = "1.0.0-RC.4")
public static final String LEGACY_TABLE_NAME = "sqlWorkbenchResultSnapshot";
@QField(isEditable = false, isPrimaryKey = true)
@@ -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;
@@ -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();
@@ -169,9 +178,9 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest
/*******************************************************************************
** Owner naming rule (1.0.0-RC.3): table, possible-value source, permission
** Owner naming rule (1.0.0-RC.4): table, possible-value source, permission
** and security key names are lowercase snake_case prefixed with the qbit's
** name; the pre-RC.3 names stay available only as deprecated constants.
** name; the pre-RC.4 names stay available only as deprecated constants.
*******************************************************************************/
@Test
@SuppressWarnings("deprecation")
@@ -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);
}
/*******************************************************************************
**
*******************************************************************************/
@@ -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");
}
@@ -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<String> 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();
}
}
@@ -219,6 +219,36 @@ class NotebookServiceTest extends BaseSqlWorkbenchTest
/*******************************************************************************
** With QQQ core permission enforcement on, an ordinary user's permission set
** (getUserPermissionNames) is enough for the whole notebook flow, including
** pinning a result (a top-level snapshot insert).
*******************************************************************************/
@Test
void testUserPermissionsSufficeWithCoreEnforcement() throws Exception
{
qInstance.setEnforcePermissionsInCoreActions(true);
try
{
Integer id = (Integer) service.create(body("Enforced", List.of(cell("c1", "SQL", "SELECT * FROM person"))), as(READER)).get("id");
service.update(id, Map.of("title", "Enforced v2"), as(READER));
SqlExecutionEngine engine = new SqlExecutionEngine(qInstance, SqlWorkbenchRuntime.get(qInstance));
Map<String, Object> response = engine.execute(ExecutionRequest.readOnly(BACKEND_NAME, "SELECT * FROM person", Map.of()), as(READER));
Map<String, Object> snapshot = service.createSnapshot(id, Map.of("executionUuid", response.get("executionUuid"), "cellKey", "c1"), as(READER), "SELECT * FROM person");
assertThat(snapshot.get("id")).isNotNull();
service.deleteSnapshot(id, (Integer) snapshot.get("id"), as(READER));
service.delete(id, as(READER));
}
finally
{
qInstance.setEnforcePermissionsInCoreActions(null);
}
}
/*******************************************************************************
**
*******************************************************************************/