Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d864eb996d | ||
|
|
c59b2ae72f | ||
|
|
103ef543d1
|
||
|
|
84a7033ae4
|
||
|
|
316a5d6f71 | ||
|
|
236fa901cf
|
||
|
|
5600496cce | ||
|
|
b0b027a9aa
|
||
|
|
4d5139cab6 | ||
|
|
3ca94f7da5 | ||
|
|
cfb176e3ac | ||
|
|
c858bcb720
|
||
|
|
e156a710e3
|
||
|
|
9ca854e59a | ||
|
|
6029174344 | ||
|
|
1ffc663ba5 | ||
|
|
dd4bd63b2b
|
No files matched your search
+2
-13
@@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
@@ -22,7 +22,7 @@ workflows:
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release\/.*|hotfix\/.*)/
|
||||
ignore: /(develop|release\/.*)/
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
@@ -58,14 +58,3 @@ workflows:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [/^v\d+\.\d+\.\d+$/]
|
||||
|
||||
publish_hotfix_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: hotfix
|
||||
filters:
|
||||
branches:
|
||||
only: [/hotfix\/.*/]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
|
||||
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
|
||||
# the release line and the release/* push runs CircleCI publish_release_candidate.
|
||||
name: Release candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Print the plan without merging or creating branches
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [develop]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
@@ -24,7 +24,7 @@ How tests are split:
|
||||
|
||||
**Entry point:** `SqlWorkbenchQBitProducer implements QBitMetaDataProducer<SqlWorkbenchQBitConfig>`.
|
||||
- Component producers are found in the `metadata` package only (`getPackageNameForFindingMetaDataProducers`), because `api` needs the optional Javalin dependency.
|
||||
- `postProduceActions` builds `SqlWorkbenchRuntime` and calls `api.SqlWorkbenchRoutes.register` when Javalin is on the classpath.
|
||||
- `postProduceActions` builds `SqlWorkbenchRuntime` and calls `api.SqlWorkbenchEndpoints.register` when Javalin is on the classpath.
|
||||
|
||||
**Packages:**
|
||||
- `sql/`: `SqlScanner` (dialect-aware lexer), `SqlClassifier` (split, then classify as READ, EXPLAIN_ANALYZE, WRITE, DDL, or FORBIDDEN), `NamedParameterSql` (`:name` → `?`). Pure Java.
|
||||
@@ -38,7 +38,7 @@ How tests are split:
|
||||
- `explain/`: `PlanParser` (Postgres JSON, MySQL JSON/text, SQLite rows, text) and `PlanNode`.
|
||||
- `schema/`: `SchemaIntrospector` (JDBC `DatabaseMetaData` plus the QQQ table and field to SQL name mapping), cached in the runtime.
|
||||
- `notebook/`: `NotebookService` (CRUD, sharing, versions in one transaction, snapshots), run as the caller's session through the standard table actions; `NotebookVersioning` (one version per change, as the system user).
|
||||
- `api/`: `SqlWorkbenchApi` (transport-agnostic, permission checks), `SqlWorkbenchRouteProvider` (Javalin 7 routes under `/qqq/v1/sql-workbench`), `ResultExporter` (CSV/JSON/XLSX via fastexcel), and `SqlWorkbenchRoutes`.
|
||||
- `api/`: `SqlWorkbenchApi` (transport-agnostic, permission checks), `SqlWorkbenchJsonSpec` and concrete endpoint specs (the UI API extension under `/qqq/v1/sql-workbench`), `ResultExporter` (CSV/JSON/XLSX via fastexcel), and `SqlWorkbenchEndpoints`.
|
||||
- `metadata/`: the 6 tables (record locks, joins, child-list widgets), the security key types, the PVSs, the record actions, the `SQL_WORKBENCH` widget and its renderer, the home dashboard widgets, the Runner child app, and the `sqlWorkbench` app (nav: Runner, then the main tables only).
|
||||
- `home/`: the dashboard data (`SqlWorkbenchHome`, read through core actions in the viewer's session so locks apply), its renderer, and inline-styled HTML helpers.
|
||||
- `SqlWorkbenchIntegrations`: opt-in ESB and Quick Search hooks for the main tables.
|
||||
@@ -65,7 +65,7 @@ How tests are split:
|
||||
## Dependencies
|
||||
|
||||
- Java 21.
|
||||
- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.4` ahead of it (RC.4 brings the record developer panel contract), as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
|
||||
- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.6` ahead of it (RC.6 brings the UI API extension contract), as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
|
||||
- `qqq-backend-core`, `qqq-backend-module-rdbms`, and `qqq-middleware-javalin` (optional).
|
||||
- Tests: JUnit 5, AssertJ, Mockito, H2, `qqq-backend-module-sqlite`/`-postgres` (drivers), Testcontainers 1.21.4.
|
||||
|
||||
|
||||
+13
-4
@@ -2,9 +2,10 @@
|
||||
|
||||
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
|
||||
- OpenAPI coverage for all 21 SQL Workbench UI API operations, including streaming CSV/JSON/XLSX export. The `sql-workbench` extension preserves `/qqq/v1/sql-workbench`, existing request/response shapes, permission and ownership checks, exact decimal parameters, execution limits and audit. The generated fragment is in `src/main/resources/openapi/qqq-v1/sql-workbench.yaml`; strict-server security tests and a fragment contract test guard it.
|
||||
- **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.
|
||||
- **Runner.** The notebook editor (`SQL_WORKBENCH` widget) moves to its own nav entry: the `sqlWorkbenchRunner` child app, labeled Runner. Open a notebook with `/app/sqlWorkbenchRunner?notebook=<id>`.
|
||||
- On a notebook's record view: an Open in Runner widget, and a Runs child list (its executions the viewer may see) through the new join `sql_workbench_notebook_join_sql_workbench_execution`.
|
||||
@@ -12,17 +13,25 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
- **Record developer panel** "SQL Workbench" (`SqlWorkbenchDeveloperPanelProvider`, registered by `SqlWorkbenchDeveloperPanelsMetaDataProducer`): read-only `SELECT`s for the record and its table in the backend's own names, backend details, and links that open them in the Runner as a new cell (`/app/sqlWorkbenchRunner?sql=&backend=`, never run). Requires QQQ 4.1.0-RC.4 (the developer panel contract) and a Next UI with the Runner `?sql=` support.
|
||||
|
||||
### Changed
|
||||
- Require QQQ `4.1.0-RC.6` for the UI API extension contract. Replace the raw Javalin route provider with endpoint specs registered by the qbit producer. `registerRoutes=false` still disables HTTP registration. No existing operation wire changes.
|
||||
- **The nav lists only the main tables**: Notebooks and the execution audit, after the Runner. Cells, versions, pinned results and shares are child lists on a notebook (still standard tables, reachable by URL and API). The app's widget list is now the dashboard, not the `SQL_WORKBENCH` widget; links to `/app/sqlWorkbench` that expected the editor should point at `/app/sqlWorkbenchRunner`.
|
||||
- The notebook record's header shows the description.
|
||||
- **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)).
|
||||
|
||||
@@ -16,7 +16,7 @@ The full design (architecture, data model, security model, limits, API contract,
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 21 and QQQ 4.1 (built against `4.1.0-RC.4`, which is required: it lets partial edits of child rows pass joined record locks, [qqq#1073](https://github.com/QRun-IO/qqq/issues/1073)).
|
||||
- Java 21 and QQQ `4.1.0-RC.6` or later (the UI API extension contract, qqq#1128).
|
||||
- `qqq-backend-module-rdbms` for the targets. `qqq-middleware-javalin` for the REST routes; it is optional, and without it the engine still works but no routes are registered.
|
||||
- A Next UI release that renders the `SQL_WORKBENCH` widget.
|
||||
|
||||
@@ -51,7 +51,7 @@ The producer adds the following to the instance:
|
||||
- **The `sqlWorkbenchWidget` widget** (type `SQL_WORKBENCH`): the notebook editor.
|
||||
- **The home dashboard widgets** ([App home](#app-home)).
|
||||
- **The `sqlWorkbench` app.** Its home page is the dashboard. Its nav lists the **Runner** (`sqlWorkbenchRunner`, a child app whose page is the `SQL_WORKBENCH` widget), then the main tables: Notebooks (`sql_workbench_notebook`) and the execution audit (`sql_workbench_execution`). Cells, versions, pinned results and shares are not in the nav; they are child lists on a notebook.
|
||||
- **The `/qqq/v1/sql-workbench/*` routes**, registered through `QJavalinMetaData`, as `qqq-esb` does.
|
||||
- **All 21 `/qqq/v1/sql-workbench/*` operations**, registered as the `sql-workbench` UI API extension. The producer preserves the established prefix; strict UI API route checking is supported. The generated [OpenAPI fragment](src/main/resources/openapi/qqq-v1/sql-workbench.yaml) covers JSON and streaming exports. See [the contract and operation catalog](docs/UI-API.md).
|
||||
|
||||
To move the app under another app (for example an Admin app), re-parent `SqlWorkbenchQBitProducer.APP_NAME` after producing; the Runner moves with it. Open a notebook in the Runner with `/app/sqlWorkbenchRunner?notebook=<id>`.
|
||||
|
||||
@@ -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. |
|
||||
@@ -231,7 +231,7 @@ A checklist for adding the workbench to a QQQ application. Each row points to th
|
||||
| Item | What to do |
|
||||
|---|---|
|
||||
| Coordinates | `com.kingsrook.qbits:qbit-sql-workbench`. Current release candidate: `1.0.0-RC.2` (Maven Central, published from `release/1.0`). |
|
||||
| QQQ version | QQQ 4.1, `4.1.0-RC.4` or later (qqq#1073: partial edits of child rows under joined locks). Java 21. |
|
||||
| QQQ version | QQQ `4.1.0-RC.6` or later (qqq#1128: UI API extension specs). Java 21. |
|
||||
| Dependencies | `qqq-backend-module-rdbms` comes in transitively. Add `qqq-middleware-javalin` yourself for the REST routes; it is optional in this qbit's POM, and without it no routes are registered. |
|
||||
| Frontend | A Next UI (`qqq-frontend-next`) release that renders the `SQL_WORKBENCH` widget: `v1.0.0-RC.13` or later. Other frontends show the app and tables but not the notebook. |
|
||||
| Registration | `new SqlWorkbenchQBitProducer().withQBitConfig(config).produce(qInstance)` after your backends are in the instance. The producer registers its own QBit metadata; don't add it by hand. To nest the app (for example under Admin), re-parent `SqlWorkbenchQBitProducer.APP_NAME` after producing. |
|
||||
@@ -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
@@ -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` |
|
||||
|
||||
+3
-3
@@ -42,7 +42,7 @@ SQL Workbench is a super-user SQL notebook for any QQQ application. Users write
|
||||
│ └ WidgetRenderer │◄───────────────►│ /qqq/v1/metaData (app + widget, filtered │
|
||||
│ case 'SQL_WORKBENCH' │ │ by sql_workbench.hasAccess)│
|
||||
│ └ SqlWorkbenchWidget (lazy) │ │ │
|
||||
│ ├ NotebookList │ /qqq/v1/ │ SqlWorkbenchRouteProvider │
|
||||
│ ├ NotebookList │ /qqq/v1/ │ SQL Workbench endpoint specs │
|
||||
│ ├ NotebookEditor │ sql-workbench/*│ └ SqlWorkbenchApi (session, perms, JSON) │
|
||||
│ │ ├ SqlCell (CM6) │◄───────────────►│ ├ SqlExecutionEngine ──► ConnectionMgr │
|
||||
│ │ ├ MarkdownCell │ │ │ ├ SqlClassifier / NamedParameterSql│
|
||||
@@ -73,8 +73,8 @@ SQL Workbench is a super-user SQL notebook for any QQQ application. Users write
|
||||
| `explain/*` | `ExplainService` (per-dialect EXPLAIN syntax) and `PlanParser`, which normalizes Postgres JSON, MySQL JSON, SQLite `EXPLAIN QUERY PLAN` rows, and H2 text into one `PlanNode` tree. |
|
||||
| `notebook/NotebookService` | Notebook CRUD, visibility, versions, and snapshots through QQQ actions on the qbit tables. |
|
||||
| `api/SqlWorkbenchApi` | Transport-agnostic API: map in, map out. Checks permissions and throws QQQ exceptions. Unit-testable without HTTP. |
|
||||
| `api/SqlWorkbenchRouteProvider` | `QJavalinRouteProviderInterface`. Sets up the session (`ExecutorSessionUtils.setupSession`), calls the API, writes JSON, and maps errors through `QJavalinUtils.handleException`. Clears the context in `finally`. |
|
||||
| `api/SqlWorkbenchJavalinMetaDataProducer` | Adds the route provider to `QJavalinMetaData` once (the ESB pattern). Called from `postProduceActions` when the class is loadable. |
|
||||
| `api/SqlWorkbenchEndpoints` and `*Spec` | One `MiddlewareSpecExtension`, `sql-workbench`, at `/sql-workbench`. Twenty JSON specs extend `AbstractJsonEndpointSpec`; export extends `AbstractEndpointSpec` directly. Core sets up and clears the session. The shared JSON transport checks access before parsing and keeps exact decimal parameters and 409 duplicate-execution errors. Existing services retain permission checks, limits and audit. See [UI API contract](UI-API.md). |
|
||||
| `api/SqlWorkbenchSchemas` | Versioned request and response component schemas, declared once by the capabilities spec for the complete extension. The committed fragment is checked by `SqlWorkbenchApiContractTest`. |
|
||||
|
||||
### 2.2 Frontend (Next UI)
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# SQL Workbench UI API contract
|
||||
|
||||
All 21 operations are registered by `SqlWorkbenchQBitProducer` through
|
||||
`SqlWorkbenchEndpoints` as one `MiddlewareSpecExtension` named `sql-workbench`,
|
||||
version `1.0.0`, with the established `/sql-workbench` prefix. Full URLs remain
|
||||
`/qqq/v1/sql-workbench/...`. QQQ `4.1.0-RC.6` is the minimum core version.
|
||||
`registerRoutes=false` disables extension registration; middleware remains optional.
|
||||
|
||||
The producer registers no raw routes. Twenty JSON operations extend
|
||||
`AbstractJsonEndpointSpec` through `SqlWorkbenchJsonSpec`; `ExportResultSpec`
|
||||
extends `AbstractEndpointSpec` directly and streams CSV, JSON or XLSX with the
|
||||
existing attachment filename and `Cache-Control: no-store` headers.
|
||||
|
||||
The checked-in fragment is
|
||||
[`src/main/resources/openapi/qqq-v1/sql-workbench.yaml`](../src/main/resources/openapi/qqq-v1/sql-workbench.yaml).
|
||||
It is also included in a host's `/qqq/v1/openapi.yaml` and `.json`, and the
|
||||
extension appears in `/qqq/v1/version`. All component names begin with
|
||||
`SqlWorkbench`. Schemas explicitly describe the existing map payloads rather
|
||||
than changing their serialization. SQL results include nullable scalar values,
|
||||
binary previews and nested arrays; execute responses vary by execution status.
|
||||
|
||||
## Compatibility and security
|
||||
|
||||
No existing operation paths, methods, request or response shapes, status codes,
|
||||
or export headers change. Next UI can vendor the fragment without adapting its
|
||||
existing calls. New metadata consists of the extension/version advertisement,
|
||||
OpenAPI schemas and stable operation identifiers.
|
||||
|
||||
Core authenticates the caller before each spec runs and clears `QContext` after
|
||||
the request. Workbench access is checked before parsing bodies, path integers or
|
||||
export formats. JSON object parsing retains `BigDecimal` precision, JSON-only
|
||||
content-type checks, empty/null-body behavior, and existing error messages.
|
||||
Duplicate execution UUIDs still return 409. SQL failures remain HTTP 200 with a
|
||||
`FAILED`, `TIMED_OUT` or `CANCELLED` execution status.
|
||||
|
||||
Specs call the same `SqlWorkbenchApi`, notebook service and execution engine.
|
||||
Write/EXPLAIN ANALYZE gates, sharing permissions, caller record locks, notebook
|
||||
ownership, cached-result ownership, cancellation ownership, per-user concurrency
|
||||
limits, audit-before-SQL and deferred audit updates remain in place. Client IPs
|
||||
continue into execution audits. No request bodies or result data are logged by
|
||||
the transport.
|
||||
|
||||
## Operations
|
||||
|
||||
| Method | Path (after `/qqq/v1/sql-workbench`) | operationId |
|
||||
|---|---|---|
|
||||
| GET | `/capabilities` | `sqlWorkbenchGetCapabilities` |
|
||||
| POST | `/execute` | `sqlWorkbenchExecute` |
|
||||
| POST | `/executions/{uuid}/cancel` | `sqlWorkbenchCancelExecution` |
|
||||
| GET | `/executions/{uuid}/export` | `sqlWorkbenchExportResult` |
|
||||
| GET | `/executions/{uuid}/rows` | `sqlWorkbenchGetRows` |
|
||||
| POST | `/explain` | `sqlWorkbenchExplain` |
|
||||
| GET | `/history` | `sqlWorkbenchGetHistory` |
|
||||
| GET | `/notebooks` | `sqlWorkbenchListNotebooks` |
|
||||
| POST | `/notebooks` | `sqlWorkbenchCreateNotebook` |
|
||||
| DELETE | `/notebooks/{id}` | `sqlWorkbenchDeleteNotebook` |
|
||||
| GET | `/notebooks/{id}` | `sqlWorkbenchGetNotebook` |
|
||||
| PUT | `/notebooks/{id}` | `sqlWorkbenchUpdateNotebook` |
|
||||
| POST | `/notebooks/{id}/duplicate` | `sqlWorkbenchDuplicateNotebook` |
|
||||
| POST | `/notebooks/{id}/publish` | `sqlWorkbenchPublishNotebook` |
|
||||
| GET | `/notebooks/{id}/snapshots` | `sqlWorkbenchListSnapshots` |
|
||||
| POST | `/notebooks/{id}/snapshots` | `sqlWorkbenchCreateSnapshot` |
|
||||
| DELETE | `/notebooks/{id}/snapshots/{snapshotId}` | `sqlWorkbenchDeleteSnapshot` |
|
||||
| GET | `/notebooks/{id}/versions` | `sqlWorkbenchListVersions` |
|
||||
| GET | `/notebooks/{id}/versions/{n}` | `sqlWorkbenchGetVersion` |
|
||||
| POST | `/notebooks/{id}/versions/{n}/restore` | `sqlWorkbenchRestoreVersion` |
|
||||
| GET | `/schema/{backendName}` | `sqlWorkbenchGetSchema` |
|
||||
|
||||
## Verification and regeneration
|
||||
|
||||
`SqlWorkbenchApiContractTest` regenerates the fragment in memory and checks it
|
||||
against the checked-in file using core's deterministic `UiApiContract` tooling.
|
||||
It also checks the namespace, secured specs, prefix, media types and 21 unique
|
||||
operation IDs. Intentional contract changes can regenerate the file with:
|
||||
|
||||
```sh
|
||||
./mvnw -B test -Dtest=SqlWorkbenchApiContractTest -DsqlWorkbench.openapi.update=true
|
||||
```
|
||||
|
||||
Before RC.6 is released, local verification uses the separately installed core:
|
||||
|
||||
```sh
|
||||
./mvnw -B verify -Dqqq.snapshot.version=4.1.0-ARCHRC6-SNAPSHOT
|
||||
```
|
||||
|
||||
The POM must continue to name `4.1.0-RC.6`; the local override is never a release
|
||||
dependency. On PowerShell, quote each `-D...` argument and use `mvnw.cmd`.
|
||||
|
||||
`SqlWorkbenchEndpointSpecTest` runs real HTTP through
|
||||
`QApplicationJavalinServer.withStrictUiApiRoutes(true)`. It preserves the former
|
||||
route-provider tests and adds 401/403 coverage for every operation, mutation
|
||||
denials, execution limits, audited rejection, exact decimals, SQL failures and
|
||||
all three exports. Engine tests continue to verify audit and transaction safety.
|
||||
@@ -18,6 +18,8 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
|
||||
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
|
||||
<revision>1.0.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
@@ -29,7 +31,7 @@
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<version>2.1.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -250,9 +252,9 @@
|
||||
|
||||
<profiles>
|
||||
<!-- Built against qqq 4.1 (QBitMetaDataProducer, runtime services, Javalin 7 -->
|
||||
<!-- routes; the record developer panel contract needs 4.1.0-RC.4). Until -->
|
||||
<!-- specs; the UI API extension contract needs 4.1.0-RC.6). Until -->
|
||||
<!-- qbit-build-parent 2.1.0 imports the released 4.1 BOM, this profile is -->
|
||||
<!-- active by default and imports qqq-bom-pom 4.1.0-RC.4 ahead of the -->
|
||||
<!-- active by default and imports qqq-bom-pom 4.1.0-RC.6 ahead of the -->
|
||||
<!-- parent's 4.0.0 BOM (same as qbit-quick-search, ADR-0007). -->
|
||||
<profile>
|
||||
<id>qqq-snapshot</id>
|
||||
@@ -260,7 +262,7 @@
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.4</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.6</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -42,7 +42,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
** SQL_WORKBENCH widget, sqlWorkbench app), applies the config's table
|
||||
** customizer and default backend, stamps sourceQBitName, then calls
|
||||
** {@link #postProduceActions}, which builds the runtime and registers the
|
||||
** /qqq/v1/sql-workbench Javalin routes.
|
||||
** /qqq/v1/sql-workbench UI API extension.
|
||||
**
|
||||
** Host usage:
|
||||
** new SqlWorkbenchQBitProducer().withQBitConfig(config).produce(qInstance);
|
||||
@@ -188,7 +188,7 @@ public class SqlWorkbenchQBitProducer implements QBitMetaDataProducer<SqlWorkben
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build the runtime and register the REST routes.
|
||||
** Build the runtime and register the UI API extension.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void postProduceActions(MetaDataProducerMultiOutput output, QInstance qInstance) throws QException
|
||||
@@ -203,7 +203,7 @@ public class SqlWorkbenchQBitProducer implements QBitMetaDataProducer<SqlWorkben
|
||||
boolean routes = false;
|
||||
if(Boolean.TRUE.equals(qBitConfig.getRegisterRoutes()) && isJavalinAvailable())
|
||||
{
|
||||
com.kingsrook.qbits.sqlworkbench.api.SqlWorkbenchRoutes.register(qInstance);
|
||||
com.kingsrook.qbits.sqlworkbench.api.SqlWorkbenchEndpoints.register(qInstance);
|
||||
routes = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Cancel the caller's running execution.
|
||||
*******************************************************************************/
|
||||
public class CancelExecutionSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/executions/{uuid}/cancel", "Cancel the caller's running execution"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchCancelledV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("EmptyRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.cancel(caller, input.getPathParam("uuid")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Create a personal notebook.
|
||||
*******************************************************************************/
|
||||
public class CreateNotebookSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/notebooks", "Create a personal notebook"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchNotebookV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("NotebookRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.notebooks().create(input.getBody(), caller));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Pin the caller's cached result to an owned notebook.
|
||||
*******************************************************************************/
|
||||
public class CreateSnapshotSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/notebooks/{id}/snapshots", "Pin the caller's cached result to an owned notebook"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchSnapshotV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("SnapshotRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.createSnapshot(caller, pathInt(input, "id"), input.getBody()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Delete an owned notebook.
|
||||
*******************************************************************************/
|
||||
public class DeleteNotebookSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.DELETE, "/notebooks/{id}", "Delete an owned notebook"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchDeletedV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
api.notebooks().delete(pathInt(input, "id"), caller);
|
||||
return (Map.of("deleted", true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Delete a snapshot from an owned notebook.
|
||||
*******************************************************************************/
|
||||
public class DeleteSnapshotSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.DELETE, "/notebooks/{id}/snapshots/{snapshotId}", "Delete a snapshot from an owned notebook"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchDeletedV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
api.notebooks().deleteSnapshot(pathInt(input, "id"), pathInt(input, "snapshotId"), caller);
|
||||
return (Map.of("deleted", true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Make a personal copy of a visible notebook.
|
||||
*******************************************************************************/
|
||||
public class DuplicateNotebookSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/notebooks/{id}/duplicate", "Make a personal copy of a visible notebook"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchNotebookV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("EmptyRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.notebooks().duplicate(pathInt(input, "id"), caller));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute read-only SQL or preview and confirm a gated write.
|
||||
*******************************************************************************/
|
||||
public class ExecuteSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/execute", "Execute read-only SQL or preview and confirm a gated write"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchExecutionV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("ExecuteRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.execute(caller, input.getBody()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Explain SQL; ANALYZE remains subject to write gates.
|
||||
*******************************************************************************/
|
||||
public class ExplainSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/explain", "Explain SQL; ANALYZE remains subject to write gates"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchExplainV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("ExplainRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.explain(caller, input.getBody()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.ResultCache;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.executors.AbstractMiddlewareExecutor;
|
||||
import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareInput;
|
||||
import com.kingsrook.qqq.middleware.javalin.executors.io.AbstractMiddlewareOutputInterface;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Method;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.HttpStatus;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stream a caller-owned cached result without buffering it as a JSON response.
|
||||
*******************************************************************************/
|
||||
public class ExportResultSpec extends AbstractEndpointSpec<ExportResultSpec.ExportInput, ExportResultSpec.ExportOutput, ExportResultSpec.ExportExecutor>
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the existing download endpoint.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (SqlWorkbenchJsonSpec.operation(HttpMethod.GET, "/executions/{uuid}/export", "Export the caller's cached result as CSV, JSON or XLSX"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stable identifier for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void customizeMethod(Method method)
|
||||
{
|
||||
method.setOperationId("sqlWorkbenchExportResult");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe UUID and format selection.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (SqlWorkbenchJsonSpec.parametersFor(defineBasicOperation().getPath(), "format"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** All three media types are served by the same operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<BasicResponse> defineAdditionalBasicResponses()
|
||||
{
|
||||
List<BasicResponse> responses = new ArrayList<>(SqlWorkbenchJsonSpec.errors());
|
||||
for(ResultExporter.Format format : ResultExporter.Format.values())
|
||||
{
|
||||
responses.add(new BasicResponse(format.getContentType(), HttpStatus.OK,
|
||||
"Attachment named sql-result-{short UUID}.{format}; Cache-Control: no-store",
|
||||
format == ResultExporter.Format.JSON ? "SqlWorkbenchExportJsonV1" : "SqlWorkbenchExportFileV1", null));
|
||||
}
|
||||
return (responses);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Check access before parsing format, just as the previous route did.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ExportInput buildInput(Context context) throws Exception
|
||||
{
|
||||
ExportInput input = new ExportInput();
|
||||
input.caller = new SqlWorkbenchApi().caller(context.ip());
|
||||
input.uuid = context.pathParam("uuid");
|
||||
input.format = ResultExporter.Format.of(StringUtils.hasContent(context.queryParam("format")) ? context.queryParam("format") : "csv");
|
||||
return (input);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stream only after the executor authorizes access to the cached result.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void handleOutput(Context context, ExportOutput output) throws Exception
|
||||
{
|
||||
String shortId = output.uuid.length() > 8 ? output.uuid.substring(0, 8) : output.uuid;
|
||||
context.contentType(output.format.getContentType());
|
||||
context.header("Content-Disposition", "attachment; filename=\"sql-result-" + shortId.replaceAll("[^A-Za-z0-9-]", "") + "." + output.format.getExtension() + "\"");
|
||||
context.header("Cache-Control", "no-store");
|
||||
ResultExporter.write(output.entry.result(), output.format, context.outputStream());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Request state populated after session authentication.
|
||||
*******************************************************************************/
|
||||
public static class ExportInput extends AbstractMiddlewareInput
|
||||
{
|
||||
private Caller caller;
|
||||
private String uuid;
|
||||
private ResultExporter.Format format;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Authorized cached data and download metadata.
|
||||
*******************************************************************************/
|
||||
public static class ExportOutput implements AbstractMiddlewareOutputInterface
|
||||
{
|
||||
private ResultCache.Entry entry;
|
||||
private String uuid;
|
||||
private ResultExporter.Format format;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Service authorization stays identical to JSON result paging.
|
||||
*******************************************************************************/
|
||||
public static class ExportExecutor extends AbstractMiddlewareExecutor<ExportInput, ExportOutput>
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Resolve only the caller's cached execution.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void execute(ExportInput input, ExportOutput output) throws QException
|
||||
{
|
||||
output.entry = new SqlWorkbenchApi().exportable(input.caller, input.uuid);
|
||||
output.uuid = input.uuid;
|
||||
output.format = input.format;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get caller capabilities and configured limits.
|
||||
*******************************************************************************/
|
||||
public class GetCapabilitiesSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Declare shared components once. The extension registers all specs together.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Map<String, Schema> defineComponentSchemas()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.components());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/capabilities", "Get caller capabilities and configured limits"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchCapabilitiesV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.capabilities(caller));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** List the caller's recent audited executions.
|
||||
*******************************************************************************/
|
||||
public class GetHistorySpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/history", "List the caller's recent audited executions"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters("limit", "backendName"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchHistoryV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.history(caller, input.getQueryParamInteger("limit"), input.getQueryParam("backendName")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get a visible notebook and its cells.
|
||||
*******************************************************************************/
|
||||
public class GetNotebookSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/notebooks/{id}", "Get a visible notebook and its cells"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchNotebookV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.notebooks().get(pathInt(input, "id"), caller));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Page or sort the caller's cached result.
|
||||
*******************************************************************************/
|
||||
public class GetRowsSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/executions/{uuid}/rows", "Page or sort the caller's cached result"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters("offset", "limit", "sort", "dir"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchRowsV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.rows(caller, input.getPathParam("uuid"), input.getQueryParamInteger("offset"), input.getQueryParamInteger("limit"), input.getQueryParamInteger("sort"), input.getQueryParam("dir")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Inspect a target backend schema.
|
||||
*******************************************************************************/
|
||||
public class GetSchemaSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/schema/{backendName}", "Inspect a target backend schema"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters("refresh"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchSchemaV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.schema(caller, input.getPathParam("backendName"), "true".equalsIgnoreCase(input.getQueryParam("refresh"))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get a visible notebook version and saved content.
|
||||
*******************************************************************************/
|
||||
public class GetVersionSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/notebooks/{id}/versions/{n}", "Get a visible notebook version and saved content"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchVersionV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.notebooks().version(pathInt(input, "id"), pathInt(input, "n"), caller));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** List visible personal or shared notebooks.
|
||||
*******************************************************************************/
|
||||
public class ListNotebooksSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/notebooks", "List visible personal or shared notebooks"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters("scope"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchNotebooksV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (Map.of("notebooks", api.notebooks().list(input.getQueryParam("scope"), caller)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** List snapshots of a visible notebook.
|
||||
*******************************************************************************/
|
||||
public class ListSnapshotsSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/notebooks/{id}/snapshots", "List snapshots of a visible notebook"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchSnapshotsV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (Map.of("snapshots", api.notebooks().snapshots(pathInt(input, "id"), caller)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** List versions of a visible notebook.
|
||||
*******************************************************************************/
|
||||
public class ListVersionsSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.GET, "/notebooks/{id}/versions", "List versions of a visible notebook"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchVersionsV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (Map.of("versions", api.notebooks().versions(pathInt(input, "id"), caller)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Publish or withdraw an owned notebook with share permission.
|
||||
*******************************************************************************/
|
||||
public class PublishNotebookSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/notebooks/{id}/publish", "Publish or withdraw an owned notebook with share permission"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchNotebookV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("PublishRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.notebooks().publish(pathInt(input, "id"), Boolean.TRUE.equals(input.getBody().get("shared")), caller));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Restore an owned notebook version as a new version.
|
||||
*******************************************************************************/
|
||||
public class RestoreVersionSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.POST, "/notebooks/{id}/versions/{n}/restore", "Restore an owned notebook version as a new version"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchNotebookV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("EmptyRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.notebooks().restore(pathInt(input, "id"), pathInt(input, "n"), caller));
|
||||
}
|
||||
}
|
||||
@@ -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.api;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtension;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtensionsMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Registers all SQL Workbench operations as UI API specs.
|
||||
*******************************************************************************/
|
||||
public class SqlWorkbenchEndpoints
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Register the extension idempotently.
|
||||
*******************************************************************************/
|
||||
public static void register(QInstance qInstance)
|
||||
{
|
||||
MiddlewareSpecExtensionsMetaData.ofOrWithNew(qInstance).withExtension(extension());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The complete UI API extension at its established prefix.
|
||||
*******************************************************************************/
|
||||
public static MiddlewareSpecExtension extension()
|
||||
{
|
||||
return (new MiddlewareSpecExtension().withName("sql-workbench").withPathPrefix("/sql-workbench")
|
||||
.withSemanticVersion("1.0.0").withDescription("Audited SQL execution and notebooks")
|
||||
.withSpec(GetCapabilitiesSpec.class)
|
||||
.withSpec(GetSchemaSpec.class)
|
||||
.withSpec(ExecuteSpec.class)
|
||||
.withSpec(ExplainSpec.class)
|
||||
.withSpec(GetRowsSpec.class)
|
||||
.withSpec(CancelExecutionSpec.class)
|
||||
.withSpec(GetHistorySpec.class)
|
||||
.withSpec(ListNotebooksSpec.class)
|
||||
.withSpec(CreateNotebookSpec.class)
|
||||
.withSpec(GetNotebookSpec.class)
|
||||
.withSpec(UpdateNotebookSpec.class)
|
||||
.withSpec(DeleteNotebookSpec.class)
|
||||
.withSpec(PublishNotebookSpec.class)
|
||||
.withSpec(DuplicateNotebookSpec.class)
|
||||
.withSpec(ListVersionsSpec.class)
|
||||
.withSpec(GetVersionSpec.class)
|
||||
.withSpec(RestoreVersionSpec.class)
|
||||
.withSpec(ListSnapshotsSpec.class)
|
||||
.withSpec(CreateSnapshotSpec.class)
|
||||
.withSpec(DeleteSnapshotSpec.class)
|
||||
.withSpec(ExportResultSpec.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.DuplicateExecutionException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicResponse;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.AbstractJsonEndpointSpec;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.In;
|
||||
import com.kingsrook.qqq.openapi.model.Method;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.RequestBody;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
import com.kingsrook.qqq.openapi.model.Type;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.HttpStatus;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Shared transport; the existing services retain authorization, limits and audit.
|
||||
*******************************************************************************/
|
||||
public abstract class SqlWorkbenchJsonSpec extends AbstractJsonEndpointSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Check access before parsing; preserve exact decimal parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public JsonInput buildInput(Context context) throws Exception
|
||||
{
|
||||
Caller caller = new SqlWorkbenchApi().caller(context.ip());
|
||||
WorkbenchInput input = new WorkbenchInput(this, caller);
|
||||
input.getPathParams().putAll(context.pathParamMap());
|
||||
context.queryParamMap().forEach((name, values) -> input.getQueryParams().put(name, values == null || values.isEmpty() ? null : values.get(0)));
|
||||
if(defineRequestBodySchema() != null)
|
||||
{
|
||||
input.getBody().putAll(readExactBody(context));
|
||||
}
|
||||
return (input);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Parse the established JSON object shape without losing decimal precision.
|
||||
*******************************************************************************/
|
||||
private static Map<String, Object> readExactBody(Context context) throws QBadRequestException
|
||||
{
|
||||
if(!StringUtils.hasContent(context.body()))
|
||||
{
|
||||
return (Map.of());
|
||||
}
|
||||
if(context.contentType() == null || !context.contentType().toLowerCase().startsWith("application/json"))
|
||||
{
|
||||
throw (new QBadRequestException("Request bodies must be JSON (Content-Type: application/json)."));
|
||||
}
|
||||
try
|
||||
{
|
||||
Map<String, Object> body = JsonUtils.toObject(context.body(), new TypeReference<Map<String, Object>>() {}, mapper -> mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS));
|
||||
return (body == null ? Map.of() : body);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QBadRequestException("The request body is not a JSON object."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Preserve duplicate execution UUID conflicts.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void handleException(Context context, Exception e)
|
||||
{
|
||||
QJavalinUtils.handleException(e instanceof DuplicateExecutionException ? org.eclipse.jetty.http.HttpStatus.Code.CONFLICT : null, context, e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Bodies remain optional; empty bodies retain their existing service semantics.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RequestBody defineRequestBody()
|
||||
{
|
||||
RequestBody body = super.defineRequestBody();
|
||||
return (body == null ? null : body.withRequired(false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stable operation identifiers for generated clients.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void customizeMethod(Method method)
|
||||
{
|
||||
method.setOperationId("sqlWorkbench" + getClass().getSimpleName().replace("Spec", ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Keep the established extension prefix.
|
||||
*******************************************************************************/
|
||||
static BasicOperation operation(HttpMethod method, String path, String summary)
|
||||
{
|
||||
return (new BasicOperation().withHttpMethod(method).withPath("/sql-workbench" + path)
|
||||
.withShortSummary(summary).withLongDescription(summary + ". Requires SQL Workbench access; services enforce caller permissions, ownership, execution limits and audit."));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Document existing request error statuses.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<BasicResponse> defineAdditionalBasicResponses()
|
||||
{
|
||||
return (errors());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Share error schemas with the export stream.
|
||||
*******************************************************************************/
|
||||
static List<BasicResponse> errors()
|
||||
{
|
||||
return (List.of(new BasicResponse(HttpStatus.BAD_REQUEST, "Invalid input or execution limit exceeded", "SqlWorkbenchErrorV1"),
|
||||
new BasicResponse(HttpStatus.UNAUTHORIZED, "Sign in required", "SqlWorkbenchErrorV1"),
|
||||
new BasicResponse(HttpStatus.FORBIDDEN, "Permission denied", "SqlWorkbenchErrorV1"),
|
||||
new BasicResponse(HttpStatus.NOT_FOUND, "Missing or invisible resource", "SqlWorkbenchErrorV1"),
|
||||
new BasicResponse(HttpStatus.CONFLICT, "Execution UUID already used", "SqlWorkbenchErrorV1")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Path and query parameter declarations.
|
||||
*******************************************************************************/
|
||||
protected List<Parameter> parameters(String... queries)
|
||||
{
|
||||
return (parametersFor(defineBasicOperation().getPath(), queries));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Share parameter declarations with the export stream.
|
||||
*******************************************************************************/
|
||||
static List<Parameter> parametersFor(String path, String... queries)
|
||||
{
|
||||
List<Parameter> parameters = new ArrayList<>();
|
||||
Matcher matcher = Pattern.compile("\\{([^}]+)}").matcher(path);
|
||||
while(matcher.find())
|
||||
{
|
||||
parameters.add(parameter(matcher.group(1), In.PATH));
|
||||
}
|
||||
for(String query : queries)
|
||||
{
|
||||
parameters.add(parameter(query, In.QUERY));
|
||||
}
|
||||
return (parameters);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Document parameter types and default behavior.
|
||||
*******************************************************************************/
|
||||
private static Parameter parameter(String name, In location)
|
||||
{
|
||||
Type type = Type.STRING;
|
||||
if(List.of("id", "n", "snapshotId", "offset", "limit", "sort").contains(name))
|
||||
{
|
||||
type = Type.INTEGER;
|
||||
}
|
||||
else if("refresh".equals(name))
|
||||
{
|
||||
type = Type.BOOLEAN;
|
||||
}
|
||||
return (new Parameter().withName(name).withIn(location).withRequired(location == In.PATH)
|
||||
.withDescription(parameterDescription(name)).withSchema(new Schema().withType(type)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Parameter help for generated clients.
|
||||
*******************************************************************************/
|
||||
private static String parameterDescription(String name)
|
||||
{
|
||||
switch(name)
|
||||
{
|
||||
case "id":
|
||||
return ("Notebook identifier.");
|
||||
case "n":
|
||||
return ("Notebook version number.");
|
||||
case "snapshotId":
|
||||
return ("Snapshot identifier.");
|
||||
case "uuid":
|
||||
return ("Execution UUID; case insensitive.");
|
||||
case "backendName":
|
||||
return ("Configured target backend name.");
|
||||
case "offset":
|
||||
return ("Rows to skip; default zero; negative values become zero.");
|
||||
case "limit":
|
||||
return ("Page size; nonpositive or absent uses the default. Rows cap at maxPageSize; history caps at 200 (default 50).");
|
||||
case "sort":
|
||||
return ("Optional zero-based result column index.");
|
||||
case "dir":
|
||||
return ("Sort direction: desc (case insensitive) or asc (default).");
|
||||
case "refresh":
|
||||
return ("True to bypass the schema cache; default false.");
|
||||
case "scope":
|
||||
return ("mine (default) or shared notebook library.");
|
||||
case "format":
|
||||
return ("csv (default), json or xlsx; case insensitive.");
|
||||
default:
|
||||
return (name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Preserve integer path validation.
|
||||
*******************************************************************************/
|
||||
protected static Integer pathInt(JsonInput input, String name) throws QBadRequestException
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Integer.parseInt(input.getPathParam(name)));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw (new QBadRequestException(name + " must be an integer."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Caller captured before body parsing, including the audit client IP.
|
||||
*******************************************************************************/
|
||||
protected static Caller caller(JsonInput input)
|
||||
{
|
||||
return (((WorkbenchInput) input).caller);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Per-request state; spec instances themselves are shared across requests.
|
||||
*******************************************************************************/
|
||||
private static class WorkbenchInput extends JsonInput
|
||||
{
|
||||
private final SqlWorkbenchJsonSpec spec;
|
||||
private final Caller caller;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Capture the spec and authorized caller.
|
||||
*******************************************************************************/
|
||||
private WorkbenchInput(SqlWorkbenchJsonSpec spec, Caller caller)
|
||||
{
|
||||
this.spec = spec;
|
||||
this.caller = caller;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Supply the spec to the framework's JSON executor.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public AbstractJsonEndpointSpec getSpec()
|
||||
{
|
||||
return (spec);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Preserve the established integer-query error message.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Integer getQueryParamInteger(String name) throws QBadRequestException
|
||||
{
|
||||
String value = getQueryParam(name);
|
||||
if(!StringUtils.hasContent(value))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
try
|
||||
{
|
||||
return (Integer.parseInt(value.trim()));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw (new QBadRequestException(name + " must be an integer."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchRuntime;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.DuplicateExecutionException;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.ResultCache;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.executors.ExecutorSessionUtils;
|
||||
import io.javalin.config.JavalinConfig;
|
||||
import io.javalin.http.ContentType;
|
||||
import io.javalin.http.Context;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Javalin routes for the SQL Workbench under /qqq/v1/sql-workbench (see
|
||||
** docs/DESIGN.md section 5 for the contract).
|
||||
**
|
||||
** Each request gets a session from the instance's authentication module
|
||||
** (ExecutorSessionUtils), then SqlWorkbenchApi checks permissions and does
|
||||
** the work. Errors map through QJavalinUtils.handleException: 400 bad
|
||||
** request, 401 unauthenticated, 403 not permitted, 404 unknown, 409 a
|
||||
** re-used executionUuid. POST / PUT
|
||||
** bodies must be JSON (Content-Type application/json): a cross-site form
|
||||
** cannot send that without a CORS preflight. Request and response bodies
|
||||
** are never logged.
|
||||
*******************************************************************************/
|
||||
public class SqlWorkbenchRouteProvider implements QJavalinRouteProviderInterface
|
||||
{
|
||||
public static final String BASE_PATH = "/qqq/v1/sql-workbench";
|
||||
|
||||
private QInstance qInstance;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setQInstance(QInstance qInstance)
|
||||
{
|
||||
this.qInstance = qInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptJavalinConfig(JavalinConfig config)
|
||||
{
|
||||
config.routes.get(BASE_PATH + "/capabilities", ctx -> respond(ctx, false, (api, caller, body) -> api.capabilities(caller)));
|
||||
config.routes.get(BASE_PATH + "/schema/{backendName}", ctx -> respond(ctx, false, (api, caller, body) -> api.schema(caller, ctx.pathParam("backendName"), "true".equalsIgnoreCase(ctx.queryParam("refresh")))));
|
||||
config.routes.post(BASE_PATH + "/execute", ctx -> respond(ctx, true, (api, caller, body) -> api.execute(caller, body)));
|
||||
config.routes.post(BASE_PATH + "/explain", ctx -> respond(ctx, true, (api, caller, body) -> api.explain(caller, body)));
|
||||
config.routes.get(BASE_PATH + "/executions/{uuid}/rows", ctx -> respond(ctx, false, (api, caller, body) -> api.rows(caller, ctx.pathParam("uuid"),
|
||||
intParam(ctx, "offset"), intParam(ctx, "limit"), intParam(ctx, "sort"), ctx.queryParam("dir"))));
|
||||
config.routes.get(BASE_PATH + "/executions/{uuid}/export", this::export);
|
||||
config.routes.post(BASE_PATH + "/executions/{uuid}/cancel", ctx -> respond(ctx, true, (api, caller, body) -> api.cancel(caller, ctx.pathParam("uuid"))));
|
||||
config.routes.get(BASE_PATH + "/history", ctx -> respond(ctx, false, (api, caller, body) -> api.history(caller, intParam(ctx, "limit"), ctx.queryParam("backendName"))));
|
||||
|
||||
config.routes.get(BASE_PATH + "/notebooks", ctx -> respond(ctx, false, (api, caller, body) -> Map.of("notebooks", api.notebooks().list(ctx.queryParam("scope"), caller))));
|
||||
config.routes.post(BASE_PATH + "/notebooks", ctx -> respond(ctx, true, (api, caller, body) -> api.notebooks().create(body, caller)));
|
||||
config.routes.get(BASE_PATH + "/notebooks/{id}", ctx -> respond(ctx, false, (api, caller, body) -> api.notebooks().get(id(ctx), caller)));
|
||||
config.routes.put(BASE_PATH + "/notebooks/{id}", ctx -> respond(ctx, true, (api, caller, body) -> api.notebooks().update(id(ctx), body, caller)));
|
||||
config.routes.delete(BASE_PATH + "/notebooks/{id}", ctx -> respond(ctx, false, (api, caller, body) ->
|
||||
{
|
||||
api.notebooks().delete(id(ctx), caller);
|
||||
return (Map.of("deleted", true));
|
||||
}));
|
||||
config.routes.post(BASE_PATH + "/notebooks/{id}/publish", ctx -> respond(ctx, true, (api, caller, body) -> api.notebooks().publish(id(ctx), Boolean.TRUE.equals(body.get("shared")), caller)));
|
||||
config.routes.post(BASE_PATH + "/notebooks/{id}/duplicate", ctx -> respond(ctx, true, (api, caller, body) -> api.notebooks().duplicate(id(ctx), caller)));
|
||||
config.routes.get(BASE_PATH + "/notebooks/{id}/versions", ctx -> respond(ctx, false, (api, caller, body) -> Map.of("versions", api.notebooks().versions(id(ctx), caller))));
|
||||
config.routes.get(BASE_PATH + "/notebooks/{id}/versions/{n}", ctx -> respond(ctx, false, (api, caller, body) -> api.notebooks().version(id(ctx), pathInt(ctx, "n"), caller)));
|
||||
config.routes.post(BASE_PATH + "/notebooks/{id}/versions/{n}/restore", ctx -> respond(ctx, true, (api, caller, body) -> api.notebooks().restore(id(ctx), pathInt(ctx, "n"), caller)));
|
||||
config.routes.get(BASE_PATH + "/notebooks/{id}/snapshots", ctx -> respond(ctx, false, (api, caller, body) -> Map.of("snapshots", api.notebooks().snapshots(id(ctx), caller))));
|
||||
config.routes.post(BASE_PATH + "/notebooks/{id}/snapshots", ctx -> respond(ctx, true, (api, caller, body) -> api.createSnapshot(caller, id(ctx), body)));
|
||||
config.routes.delete(BASE_PATH + "/notebooks/{id}/snapshots/{snapshotId}", ctx -> respond(ctx, false, (api, caller, body) ->
|
||||
{
|
||||
api.notebooks().deleteSnapshot(id(ctx), pathInt(ctx, "snapshotId"), caller);
|
||||
return (Map.of("deleted", true));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Session, permission check, body, handler, JSON - or the error's status.
|
||||
*******************************************************************************/
|
||||
private void respond(Context context, boolean readsBody, Handler handler)
|
||||
{
|
||||
try
|
||||
{
|
||||
ExecutorSessionUtils.setupSession(context, qInstance);
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi(qInstance, SqlWorkbenchRuntime.get(qInstance));
|
||||
Caller caller = api.caller(context.ip());
|
||||
Map<String, Object> body = readsBody ? readJsonBody(context) : Map.of();
|
||||
Object result = handler.handle(api, caller, body);
|
||||
|
||||
context.contentType(ContentType.APPLICATION_JSON);
|
||||
context.result(JsonUtils.toJsonCustomized(result, builder -> builder.serializationInclusion(JsonInclude.Include.ALWAYS)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
QJavalinUtils.handleException(e instanceof DuplicateExecutionException ? HttpStatus.Code.CONFLICT : null, context, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** GET /executions/{uuid}/export?format=csv|json|xlsx
|
||||
*******************************************************************************/
|
||||
private void export(Context context)
|
||||
{
|
||||
try
|
||||
{
|
||||
ExecutorSessionUtils.setupSession(context, qInstance);
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi(qInstance, SqlWorkbenchRuntime.get(qInstance));
|
||||
Caller caller = api.caller(context.ip());
|
||||
ResultExporter.Format format = ResultExporter.Format.of(StringUtils.hasContent(context.queryParam("format")) ? context.queryParam("format") : "csv");
|
||||
String uuid = context.pathParam("uuid");
|
||||
ResultCache.Entry entry = api.exportable(caller, uuid);
|
||||
|
||||
String shortId = uuid.length() > 8 ? uuid.substring(0, 8) : uuid;
|
||||
context.contentType(format.getContentType());
|
||||
context.header("Content-Disposition", "attachment; filename=\"sql-result-" + shortId.replaceAll("[^A-Za-z0-9-]", "") + "." + format.getExtension() + "\"");
|
||||
context.header("Cache-Control", "no-store");
|
||||
ResultExporter.write(entry.result(), format, context.outputStream());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
QJavalinUtils.handleException(null, context, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A JSON object body. Decimals stay exact (BigDecimal).
|
||||
*******************************************************************************/
|
||||
static Map<String, Object> readJsonBody(Context context) throws QBadRequestException
|
||||
{
|
||||
String body = context.body();
|
||||
if(!StringUtils.hasContent(body))
|
||||
{
|
||||
return (new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
String contentType = context.contentType();
|
||||
if(contentType == null || !contentType.toLowerCase().startsWith("application/json"))
|
||||
{
|
||||
throw (new QBadRequestException("Request bodies must be JSON (Content-Type: application/json)."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Map<String, Object> map = JsonUtils.toObject(body, new TypeReference<Map<String, Object>>() {}, mapper -> mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS));
|
||||
return (map == null ? new LinkedHashMap<>() : map);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QBadRequestException("The request body is not a JSON object."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer id(Context context) throws QBadRequestException
|
||||
{
|
||||
return (pathInt(context, "id"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer pathInt(Context context, String name) throws QBadRequestException
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Integer.parseInt(context.pathParam(name)));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw (new QBadRequestException(name + " must be an integer."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer intParam(Context context, String name) throws QBadRequestException
|
||||
{
|
||||
String value = context.queryParam(name);
|
||||
if(!StringUtils.hasContent(value))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
try
|
||||
{
|
||||
return (Integer.parseInt(value.trim()));
|
||||
}
|
||||
catch(NumberFormatException e)
|
||||
{
|
||||
throw (new QBadRequestException(name + " must be an integer."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One endpoint's work.
|
||||
*******************************************************************************/
|
||||
@FunctionalInterface
|
||||
private interface Handler
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
Object handle(SqlWorkbenchApi api, Caller caller, Map<String, Object> body) throws QException;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
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.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Registers SqlWorkbenchRouteProvider (the /qqq/v1/sql-workbench routes)
|
||||
** with the Javalin server through the instance's QJavalinMetaData - as
|
||||
** qqq-esb's EsbJavalinMetaDataProducer does. Only touched when
|
||||
** qqq-middleware-javalin is on the classpath (the producer checks first).
|
||||
*******************************************************************************/
|
||||
public class SqlWorkbenchRoutes
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Add the route provider reference, once.
|
||||
*******************************************************************************/
|
||||
public static void register(QInstance qInstance)
|
||||
{
|
||||
QJavalinMetaData javalinMetaData = QJavalinMetaData.ofOrWithNew(qInstance);
|
||||
boolean registered = CollectionUtils.nonNullList(javalinMetaData.getAdditionalRouteProviderReferences()).stream()
|
||||
.anyMatch(reference -> reference != null && SqlWorkbenchRouteProvider.class.getName().equals(reference.getName()));
|
||||
if(!registered)
|
||||
{
|
||||
javalinMetaData.withAdditionalRouteProviderReference(new QCodeReference(SqlWorkbenchRouteProvider.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
import com.kingsrook.qqq.openapi.model.Type;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Versioned wire schemas shared by SQL Workbench specs.
|
||||
*******************************************************************************/
|
||||
public class SqlWorkbenchSchemas
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Explicit schemas for the existing map payloads; no serialization changes.
|
||||
*******************************************************************************/
|
||||
public static Map<String, Schema> components()
|
||||
{
|
||||
Map<String, Schema> schemas = new LinkedHashMap<>();
|
||||
schemas.put("SqlWorkbenchErrorV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("error", new Schema().withType(Type.STRING)));
|
||||
schemas.put("SqlWorkbenchEmptyRequestV1", new Schema().withType(Type.OBJECT));
|
||||
schemas.put("SqlWorkbenchDeletedV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("deleted", new Schema().withType(Type.BOOLEAN)));
|
||||
schemas.put("SqlWorkbenchCancelledV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("cancelled", new Schema().withType(Type.BOOLEAN)));
|
||||
schemas.put("SqlWorkbenchLimitsV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("maxRows", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("pageSize", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("maxPageSize", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("maxResultBytes", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("statementTimeoutSeconds", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("writeStatementTimeoutSeconds", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("maxAffectedRowsWithoutAcknowledgement", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("maxAffectedRows", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("maxSqlLength", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("maxStatementsPerRun", new Schema().withType(Type.INTEGER).withFormat("int64")));
|
||||
schemas.put("SqlWorkbenchBackendV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("label", new Schema().withType(Type.STRING))
|
||||
.withProperty("vendor", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("dialect", ref("Dialect"))
|
||||
.withProperty("readOnlyCredentials", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("readOnlyEnforced", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("transactionalDdl", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("writeAllowed", new Schema().withType(Type.BOOLEAN)));
|
||||
schemas.put("SqlWorkbenchCapabilitiesV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("version", new Schema().withType(Type.STRING))
|
||||
.withProperty("canWrite", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("canShare", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("writeModeEnabled", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("limits", ref("Limits"))
|
||||
.withProperty("backends", array(ref("Backend"))));
|
||||
schemas.put("SqlWorkbenchColumnV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("label", new Schema().withType(Type.STRING))
|
||||
.withProperty("typeName", new Schema().withType(Type.STRING))
|
||||
.withProperty("jdbcType", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("kind", ref("ColumnKind")));
|
||||
schemas.put("SqlWorkbenchBinaryValueV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("$binary", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("length", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("hexPreview", new Schema().withType(Type.STRING)));
|
||||
schemas.put("SqlWorkbenchStatementV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("index", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("kind", ref("StatementKind"))
|
||||
.withProperty("leadingKeyword", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("text", new Schema().withType(Type.STRING))
|
||||
.withProperty("affectedRows", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true)));
|
||||
schemas.put("SqlWorkbenchExecuteRequestV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING))
|
||||
.withProperty("sql", new Schema().withType(Type.STRING))
|
||||
.withProperty("params", ref("Parameters"))
|
||||
.withProperty("mode", new Schema().withType(Type.STRING))
|
||||
.withProperty("notebookId", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("cellKey", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("pageSize", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("confirmToken", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("acknowledgeRowGuard", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("executionUuid", new Schema().withType(Type.STRING).withNullable(true)));
|
||||
schemas.put("SqlWorkbenchExplainRequestV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING))
|
||||
.withProperty("sql", new Schema().withType(Type.STRING))
|
||||
.withProperty("params", ref("Parameters"))
|
||||
.withProperty("analyze", new Schema().withType(Type.BOOLEAN)));
|
||||
schemas.put("SqlWorkbenchExecutionV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("executionUuid", new Schema().withType(Type.STRING))
|
||||
.withProperty("status", ref("Status"))
|
||||
.withProperty("mode", new Schema().withType(Type.STRING))
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING))
|
||||
.withProperty("durationMs", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("statements", array(ref("Statement")))
|
||||
.withProperty("columns", array(ref("Column")))
|
||||
.withProperty("rows", ref("RowsData"))
|
||||
.withProperty("rowCount", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("hasMore", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("truncated", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("truncatedReason", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("rowsAffected", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("error", new Schema().withType(Type.STRING))
|
||||
.withProperty("sqlState", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("dryRunSupported", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("dryRunNote", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("totalAffectedRows", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("rowGuardExceeded", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("rowGuardLimit", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("confirmToken", new Schema().withType(Type.STRING))
|
||||
.withProperty("expiresAt", new Schema().withType(Type.STRING))
|
||||
.withProperty("previewAffectedRows", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("actualAffectedRows", new Schema().withType(Type.INTEGER).withFormat("int64")));
|
||||
schemas.put("SqlWorkbenchRowsV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("executionUuid", new Schema().withType(Type.STRING))
|
||||
.withProperty("columns", array(ref("Column")))
|
||||
.withProperty("rows", ref("RowsData"))
|
||||
.withProperty("offset", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("rowCount", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("truncated", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("truncatedReason", new Schema().withType(Type.STRING).withNullable(true)));
|
||||
schemas.put("SqlWorkbenchPlanNodeV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("label", new Schema().withType(Type.STRING))
|
||||
.withProperty("detail", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("startupCost", new Schema().withType(Type.NUMBER).withNullable(true))
|
||||
.withProperty("totalCost", new Schema().withType(Type.NUMBER).withNullable(true))
|
||||
.withProperty("planRows", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("actualTimeMs", new Schema().withType(Type.NUMBER).withNullable(true))
|
||||
.withProperty("actualRows", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("loops", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("extra", ref("FreeObject"))
|
||||
.withProperty("children", array(ref("PlanNode"))));
|
||||
schemas.put("SqlWorkbenchExplainV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("executionUuid", new Schema().withType(Type.STRING))
|
||||
.withProperty("status", ref("Status"))
|
||||
.withProperty("mode", new Schema().withType(Type.STRING))
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING))
|
||||
.withProperty("dialect", ref("Dialect"))
|
||||
.withProperty("analyze", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("plan", ref("PlanNode"))
|
||||
.withProperty("raw", new Schema().withType(Type.STRING))
|
||||
.withProperty("error", new Schema().withType(Type.STRING))
|
||||
.withProperty("sqlState", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("durationMs", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true)));
|
||||
schemas.put("SqlWorkbenchSchemaColumnV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("typeName", new Schema().withType(Type.STRING))
|
||||
.withProperty("nullable", new Schema().withType(Type.BOOLEAN)));
|
||||
schemas.put("SqlWorkbenchSchemaTableV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("type", new Schema().withType(Type.STRING))
|
||||
.withProperty("columns", array(ref("SchemaColumn"))));
|
||||
schemas.put("SqlWorkbenchDatabaseSchemaV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("tables", array(ref("SchemaTable"))));
|
||||
schemas.put("SqlWorkbenchQqqFieldV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("label", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("sqlName", new Schema().withType(Type.STRING))
|
||||
.withProperty("type", new Schema().withType(Type.STRING).withNullable(true)));
|
||||
schemas.put("SqlWorkbenchQqqTableV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("label", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("sqlName", new Schema().withType(Type.STRING))
|
||||
.withProperty("fields", array(ref("QqqField"))));
|
||||
schemas.put("SqlWorkbenchSchemaV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING))
|
||||
.withProperty("dialect", ref("Dialect"))
|
||||
.withProperty("schemas", array(ref("DatabaseSchema")))
|
||||
.withProperty("truncated", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("keywords", array(new Schema().withType(Type.STRING)))
|
||||
.withProperty("functions", array(new Schema().withType(Type.STRING)))
|
||||
.withProperty("qqqTables", array(ref("QqqTable")))
|
||||
.withProperty("cachedAt", new Schema().withType(Type.STRING)));
|
||||
schemas.put("SqlWorkbenchHistoryEntryV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("executionUuid", new Schema().withType(Type.STRING))
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING))
|
||||
.withProperty("mode", new Schema().withType(Type.STRING))
|
||||
.withProperty("status", new Schema().withType(Type.STRING))
|
||||
.withProperty("sqlText", new Schema().withType(Type.STRING))
|
||||
.withProperty("notebookId", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("cellKey", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("startTime", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("durationMs", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("rowsReturned", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("rowsAffected", new Schema().withType(Type.INTEGER).withFormat("int64").withNullable(true))
|
||||
.withProperty("errorMessage", new Schema().withType(Type.STRING).withNullable(true)));
|
||||
schemas.put("SqlWorkbenchHistoryV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("executions", array(ref("HistoryEntry"))));
|
||||
schemas.put("SqlWorkbenchNotebookParameterV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("name", new Schema().withType(Type.STRING))
|
||||
.withProperty("type", new Schema().withType(Type.STRING))
|
||||
.withProperty("defaultValue", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("label", new Schema().withType(Type.STRING).withNullable(true)));
|
||||
schemas.put("SqlWorkbenchCellV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("cellKey", new Schema().withType(Type.STRING))
|
||||
.withProperty("cellType", new Schema().withType(Type.STRING))
|
||||
.withProperty("title", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("content", new Schema().withType(Type.STRING))
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("sortOrder", new Schema().withType(Type.INTEGER).withFormat("int64")));
|
||||
schemas.put("SqlWorkbenchNotebookSummaryV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("id", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("title", new Schema().withType(Type.STRING))
|
||||
.withProperty("description", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("visibility", new Schema().withType(Type.STRING))
|
||||
.withProperty("ownerUserId", new Schema().withType(Type.STRING))
|
||||
.withProperty("ownerName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("defaultBackendName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("currentVersion", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("canEdit", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("createDate", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("modifyDate", new Schema().withType(Type.STRING).withNullable(true)));
|
||||
schemas.put("SqlWorkbenchNotebookV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("id", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("title", new Schema().withType(Type.STRING))
|
||||
.withProperty("description", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("visibility", new Schema().withType(Type.STRING))
|
||||
.withProperty("ownerUserId", new Schema().withType(Type.STRING))
|
||||
.withProperty("ownerName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("defaultBackendName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("currentVersion", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("canEdit", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("createDate", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("modifyDate", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("parameters", array(ref("NotebookParameter")))
|
||||
.withProperty("cells", array(ref("Cell"))));
|
||||
schemas.put("SqlWorkbenchNotebookContentV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("title", new Schema().withType(Type.STRING))
|
||||
.withProperty("description", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("defaultBackendName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("parameters", array(ref("NotebookParameter")))
|
||||
.withProperty("cells", array(ref("Cell"))));
|
||||
schemas.put("SqlWorkbenchNotebookRequestV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("title", new Schema().withType(Type.STRING))
|
||||
.withProperty("description", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("defaultBackendName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("parameters", array(ref("NotebookParameter")))
|
||||
.withProperty("cells", array(ref("Cell")))
|
||||
.withProperty("comment", new Schema().withType(Type.STRING)));
|
||||
schemas.put("SqlWorkbenchNotebooksV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("notebooks", array(ref("NotebookSummary"))));
|
||||
schemas.put("SqlWorkbenchPublishRequestV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("shared", new Schema().withType(Type.BOOLEAN)));
|
||||
schemas.put("SqlWorkbenchVersionSummaryV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("versionNumber", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("savedByName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("comment", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("createDate", new Schema().withType(Type.STRING)));
|
||||
schemas.put("SqlWorkbenchVersionV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("versionNumber", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("savedByName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("comment", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("createDate", new Schema().withType(Type.STRING))
|
||||
.withProperty("notebook", ref("NotebookContent")));
|
||||
schemas.put("SqlWorkbenchVersionsV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("versions", array(ref("VersionSummary"))));
|
||||
schemas.put("SqlWorkbenchSnapshotRequestV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("executionUuid", new Schema().withType(Type.STRING))
|
||||
.withProperty("cellKey", new Schema().withType(Type.STRING))
|
||||
.withProperty("label", new Schema().withType(Type.STRING).withNullable(true)));
|
||||
schemas.put("SqlWorkbenchSnapshotResultV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("columns", array(ref("Column")))
|
||||
.withProperty("rows", ref("RowsData")));
|
||||
schemas.put("SqlWorkbenchSnapshotV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("id", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("cellKey", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("executionUuid", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("label", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("backendName", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("rowCount", new Schema().withType(Type.INTEGER).withFormat("int64"))
|
||||
.withProperty("truncated", new Schema().withType(Type.BOOLEAN))
|
||||
.withProperty("createDate", new Schema().withType(Type.STRING).withNullable(true))
|
||||
.withProperty("result", ref("SnapshotResult")));
|
||||
schemas.put("SqlWorkbenchSnapshotsV1", new Schema().withType(Type.OBJECT)
|
||||
.withProperty("snapshots", array(ref("Snapshot"))));
|
||||
schemas.put("SqlWorkbenchDialectV1", new Schema().withType(Type.STRING).withEnumValues(List.of("postgresql", "mysql", "sqlite", "h2", "sql")));
|
||||
schemas.put("SqlWorkbenchColumnKindV1", new Schema().withType(Type.STRING).withEnumValues(List.of("number", "string", "boolean", "date", "datetime", "time", "json", "binary", "array", "other")));
|
||||
schemas.put("SqlWorkbenchStatementKindV1", new Schema().withType(Type.STRING).withEnumValues(List.of("READ", "EXPLAIN_ANALYZE", "WRITE", "DDL", "FORBIDDEN")));
|
||||
schemas.put("SqlWorkbenchStatusV1", new Schema().withType(Type.STRING).withEnumValues(List.of("SUCCEEDED", "FAILED", "TIMED_OUT", "CANCELLED", "REJECTED", "PREVIEW", "DRIFTED")));
|
||||
schemas.put("SqlWorkbenchParametersV1", new Schema().withType(Type.OBJECT).withAdditionalProperties(new Schema())
|
||||
.withDescription("Named SQL parameter values. JSON decimals are parsed exactly as BigDecimal."));
|
||||
schemas.put("SqlWorkbenchFreeObjectV1", new Schema().withType(Type.OBJECT).withAdditionalProperties(true));
|
||||
schemas.put("SqlWorkbenchValueV1", new Schema().withAnyOf(List.of(
|
||||
new Schema().withType(Type.STRING).withNullable(true), new Schema().withType(Type.NUMBER), new Schema().withType(Type.BOOLEAN),
|
||||
ref("BinaryValue"), array(ref("Value")), ref("FreeObject")))
|
||||
.withDescription("JSON-safe JDBC value; null, scalar, binary preview, nested array or vendor JSON object."));
|
||||
schemas.put("SqlWorkbenchRowsDataV1", array(array(ref("Value"))));
|
||||
schemas.put("SqlWorkbenchExportFileV1", new Schema().withType(Type.STRING).withFormat("binary"));
|
||||
schemas.put("SqlWorkbenchExportJsonV1", array(new Schema().withType(Type.OBJECT).withAdditionalProperties(ref("Value")))
|
||||
.withDescription("Rows keyed by unique column labels, with duplicate labels suffixed."));
|
||||
return (schemas);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Reference a component in this extension's namespace.
|
||||
*******************************************************************************/
|
||||
public static Schema ref(String name)
|
||||
{
|
||||
return (new Schema().withRefToSchema("SqlWorkbench" + name + "V1"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An array with explicitly described items.
|
||||
*******************************************************************************/
|
||||
private static Schema array(Schema items)
|
||||
{
|
||||
return (new Schema().withType(Type.ARRAY).withItems(items));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.sqlworkbench.engine.Caller;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.BasicOperation;
|
||||
import com.kingsrook.qqq.openapi.model.HttpMethod;
|
||||
import com.kingsrook.qqq.openapi.model.Parameter;
|
||||
import com.kingsrook.qqq.openapi.model.Schema;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Update an owned notebook and record a version.
|
||||
*******************************************************************************/
|
||||
public class UpdateNotebookSpec extends SqlWorkbenchJsonSpec
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Describe the operation.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public BasicOperation defineBasicOperation()
|
||||
{
|
||||
return (operation(HttpMethod.PUT, "/notebooks/{id}", "Update an owned notebook and record a version"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe path and query parameters.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<Parameter> defineRequestParameters()
|
||||
{
|
||||
return (parameters());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Name the response schema.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected String getResponseSchemaName()
|
||||
{
|
||||
return ("SqlWorkbenchNotebookV1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Describe the JSON body.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Schema defineRequestBodySchema()
|
||||
{
|
||||
return (SqlWorkbenchSchemas.ref("NotebookRequest"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Invoke the existing service with the authenticated caller.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected Object handle(JsonInput input) throws Exception
|
||||
{
|
||||
SqlWorkbenchApi api = new SqlWorkbenchApi();
|
||||
Caller caller = caller(input);
|
||||
return (api.notebooks().update(pathInt(input, "id"), input.getBody(), caller));
|
||||
}
|
||||
}
|
||||
@@ -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", "");
|
||||
|
||||
+2
-1
@@ -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))
|
||||
{
|
||||
|
||||
+25
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -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);
|
||||
|
||||
+12
-5
@@ -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)))
|
||||
|
||||
+69
@@ -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)
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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,8 +34,20 @@ 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 com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtensionsMetaData;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -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);
|
||||
@@ -132,7 +137,7 @@ class SqlWorkbenchQBitProducerTest extends BaseSqlWorkbenchTest
|
||||
assertThat(runner.getPermissionRules().getPermissionBaseName()).isEqualTo("sql_workbench");
|
||||
assertThat(app.getChildren()).noneMatch(child -> child instanceof QProcessMetaData);
|
||||
|
||||
assertThat(QJavalinMetaData.of(qInstance).getAdditionalRouteProviderReferences()).isNotEmpty();
|
||||
assertThat(MiddlewareSpecExtensionsMetaData.ofOrWithNew(qInstance).getExtensions()).isNotEmpty();
|
||||
assertThat(SqlWorkbenchRuntime.get(qInstance)).isNotNull();
|
||||
assertThat(new SqlWorkbenchWidgetRenderer().render(null).getWidgetData().getType()).isEqualTo("SQL_WORKBENCH");
|
||||
assertThat(SqlWorkbenchQBitProducer.getVersion()).isNotBlank();
|
||||
@@ -155,7 +160,11 @@ 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(QJavalinMetaData.of(instance)).isNull();
|
||||
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(MiddlewareSpecExtensionsMetaData.of(instance)).isNull();
|
||||
|
||||
SqlWorkbenchRuntime first = prefixed.getRuntime();
|
||||
new SqlWorkbenchQBitProducer().withQBitConfig(prefixed).getQBitMetaData();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
|
||||
import com.kingsrook.qqq.middleware.javalin.tools.UiApiContract;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The committed fragment describes exactly the shipped extension operations.
|
||||
*******************************************************************************/
|
||||
class SqlWorkbenchApiContractTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Regenerate with -DsqlWorkbench.openapi.update=true after intentional changes.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void contract() throws Exception
|
||||
{
|
||||
var extension = SqlWorkbenchEndpoints.extension();
|
||||
var version = new MiddlewareVersionV1();
|
||||
String generated = UiApiContract.generateExtensionYaml(version, extension);
|
||||
Path path = Path.of("src/main/resources/openapi/qqq-v1/sql-workbench.yaml");
|
||||
if(Boolean.getBoolean("sqlWorkbench.openapi.update"))
|
||||
{
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, generated);
|
||||
}
|
||||
assertThat(UiApiContract.check(generated, Files.readString(path), null, 1,
|
||||
"./mvnw -Dtest=SqlWorkbenchApiContractTest -DsqlWorkbench.openapi.update=true test")).isEmpty();
|
||||
var model = version.generateExtensionOpenAPIModel("qqq", extension);
|
||||
assertThat(model.getComponents().getSchemas().keySet()).allMatch(name -> name.startsWith("SqlWorkbench"));
|
||||
assertThat(model.getPaths().keySet()).allMatch(name -> name.startsWith("/qqq/v1/sql-workbench/"));
|
||||
assertThat(extension.getSpecs()).hasSize(21);
|
||||
assertThat(extension.getEffectivePathPrefix()).isEqualTo("/sql-workbench");
|
||||
Set<String> operationIds = new HashSet<>();
|
||||
Set<String> componentNames = new HashSet<>();
|
||||
for(var reference : extension.getSpecs())
|
||||
{
|
||||
var spec = (com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec<?, ?, ?>) Class.forName(reference.getName()).getConstructor().newInstance();
|
||||
assertThat(spec.isSecured()).isTrue();
|
||||
assertThat(spec.defineMethod().getOperationId()).startsWith("sqlWorkbench");
|
||||
assertThat(operationIds.add(spec.defineMethod().getOperationId())).isTrue();
|
||||
for(String name : spec.defineComponentSchemas().keySet())
|
||||
{
|
||||
assertThat(componentNames.add(name)).as("Component declared once: " + name).isTrue();
|
||||
}
|
||||
}
|
||||
assertThat(generated).contains("text/csv; charset=utf-8", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "format: \"binary\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qbits.sqlworkbench.BaseSqlWorkbenchTest;
|
||||
import com.kingsrook.qbits.sqlworkbench.SqlWorkbenchRuntime;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.AbstractEndpointSpec;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.extensions.MiddlewareSpecExtensionsMetaData;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The routes over real HTTP (Javalin on a random port, MOCK authentication
|
||||
** with the sessionId cookie choosing the test user).
|
||||
*******************************************************************************/
|
||||
class SqlWorkbenchEndpointSpecTest extends BaseSqlWorkbenchTest
|
||||
{
|
||||
private QApplicationJavalinServer javalin;
|
||||
private String base;
|
||||
private HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void startServer() throws Exception
|
||||
{
|
||||
QContext.clear();
|
||||
javalin = new QApplicationJavalinServer(new AbstractQQQApplication()
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Serve the same instance and runtime prepared by the test fixture.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QInstance defineQInstance()
|
||||
{
|
||||
return (qInstance);
|
||||
}
|
||||
}).withPort(0).withServeFrontendNext(false).withServeFrontendMaterialDashboard(false)
|
||||
.withServeLegacyUnversionedMiddlewareAPI(false).withStrictUiApiRoutes(true);
|
||||
javalin.start();
|
||||
base = "http://localhost:" + javalin.getBoundPort() + "/qqq/v1/sql-workbench";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void stopServer()
|
||||
{
|
||||
javalin.stop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private HttpResponse<String> send(String method, String path, String user, String body, String contentType) throws Exception
|
||||
{
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(base + path)).header("Cookie", "sessionId=" + user);
|
||||
if(contentType != null)
|
||||
{
|
||||
builder.header("Content-Type", contentType);
|
||||
}
|
||||
builder.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(body));
|
||||
return (client.send(builder.build(), HttpResponse.BodyHandlers.ofString()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Map<String, Object> json(HttpResponse<String> response) throws Exception
|
||||
{
|
||||
return (JsonUtils.toObject(response.body(), new TypeReference<Map<String, Object>>() {}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRegistration()
|
||||
{
|
||||
SqlWorkbenchEndpoints.register(qInstance);
|
||||
var extensions = MiddlewareSpecExtensionsMetaData.ofOrWithNew(qInstance).getExtensions();
|
||||
assertThat(extensions).containsOnlyKeys("sql-workbench");
|
||||
assertThat(extensions.get("sql-workbench").getSpecs()).hasSize(21);
|
||||
assertThat(javalin.isStrictUiApiRoutes()).isTrue();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testEndToEnd() throws Exception
|
||||
{
|
||||
assertThat(send("GET", "/capabilities", "nobody", null, null).statusCode()).isEqualTo(403);
|
||||
assertThat(send("GET", "/capabilities", READER, null, null).statusCode()).isEqualTo(200);
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// JSON only - a form post (CSRF shape) is refused //
|
||||
////////////////////////////////////////////////////////////
|
||||
assertThat(send("POST", "/execute", READER, "backendName=h2&sql=SELECT+1", "application/x-www-form-urlencoded").statusCode()).isEqualTo(400);
|
||||
assertThat(send("POST", "/execute", READER, "not json", "application/json").statusCode()).isEqualTo(400);
|
||||
|
||||
HttpResponse<String> run = send("POST", "/execute", READER, "{\"backendName\":\"h2\",\"sql\":\"SELECT id, salary FROM person WHERE salary > :s ORDER BY id\",\"params\":{\"s\":25000.25}}", "application/json");
|
||||
assertThat(run.statusCode()).isEqualTo(200);
|
||||
Map<String, Object> result = json(run);
|
||||
assertThat(result.get("status")).isEqualTo("SUCCEEDED");
|
||||
assertThat(result.get("rowCount")).isEqualTo(2);
|
||||
String uuid = (String) result.get("executionUuid");
|
||||
|
||||
assertThat(json(send("GET", "/executions/" + uuid + "/rows?offset=1&limit=1&sort=0&dir=desc", READER, null, null)).get("rows")).asList().hasSize(1);
|
||||
assertThat(send("GET", "/executions/" + uuid + "/rows?offset=x", READER, null, null).statusCode()).isEqualTo(400);
|
||||
assertThat(send("GET", "/executions/" + uuid + "/rows", OTHER_READER, null, null).statusCode()).isEqualTo(404);
|
||||
|
||||
HttpResponse<String> csv = send("GET", "/executions/" + uuid + "/export?format=csv", READER, null, null);
|
||||
assertThat(csv.statusCode()).isEqualTo(200);
|
||||
assertThat(csv.headers().firstValue("Content-Disposition").orElse("")).contains("attachment").contains(".csv");
|
||||
assertThat(csv.body()).contains("id,salary");
|
||||
assertThat(send("GET", "/executions/" + uuid + "/export?format=pdf", READER, null, null).statusCode()).isEqualTo(400);
|
||||
assertThat(send("GET", "/executions/" + uuid + "/export", OTHER_READER, null, null).statusCode()).isEqualTo(404);
|
||||
|
||||
assertThat(send("POST", "/execute", READER, "{\"backendName\":\"h2\",\"sql\":\"DELETE FROM person\",\"mode\":\"WRITE\"}", "application/json").statusCode()).isEqualTo(403);
|
||||
assertThat(send("POST", "/execute", READER, "{\"backendName\":\"nope\",\"sql\":\"SELECT 1\"}", "application/json").statusCode()).isEqualTo(404);
|
||||
assertThat(json(send("POST", "/explain", READER, "{\"backendName\":\"h2\",\"sql\":\"SELECT 1\"}", "application/json")).get("status")).isEqualTo("SUCCEEDED");
|
||||
assertThat(json(send("POST", "/executions/" + uuid + "/cancel", READER, "{}", "application/json")).get("cancelled")).isEqualTo(false);
|
||||
assertThat((List<?>) json(send("GET", "/history?limit=5", READER, null, null)).get("executions")).hasSize(3);
|
||||
assertThat(json(send("GET", "/schema/h2", READER, null, null)).get("dialect")).isEqualTo("h2");
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// a client-chosen id: any case on the paths, single-use (409) //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
String clientUuid = "3F2504E0-4F89-11D3-9A0C-0305E82C3301";
|
||||
String body = "{\"backendName\":\"h2\",\"sql\":\"SELECT 1 AS one\",\"executionUuid\":\"" + clientUuid + "\"}";
|
||||
assertThat(json(send("POST", "/execute", READER, body, "application/json")).get("executionUuid")).isEqualTo(clientUuid.toLowerCase());
|
||||
assertThat(send("GET", "/executions/" + clientUuid + "/rows", READER, null, null).statusCode()).isEqualTo(200);
|
||||
assertThat(send("GET", "/executions/" + clientUuid + "/export", READER, null, null).statusCode()).isEqualTo(200);
|
||||
HttpResponse<String> again = send("POST", "/execute", READER, body, "application/json");
|
||||
assertThat(again.statusCode()).isEqualTo(409);
|
||||
assertThat(again.body()).contains("already used");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNotebookRoutes() throws Exception
|
||||
{
|
||||
Map<String, Object> created = json(send("POST", "/notebooks", WRITER, "{\"title\":\"N1\",\"cells\":[{\"cellKey\":\"a\",\"cellType\":\"SQL\",\"content\":\"SELECT 1\"}]}", "application/json"));
|
||||
Integer id = (Integer) created.get("id");
|
||||
|
||||
assertThat((List<?>) json(send("GET", "/notebooks?scope=mine", WRITER, null, null)).get("notebooks")).hasSize(1);
|
||||
assertThat(json(send("GET", "/notebooks/" + id, WRITER, null, null)).get("title")).isEqualTo("N1");
|
||||
assertThat(send("GET", "/notebooks/" + id, READER, null, null).statusCode()).isEqualTo(404);
|
||||
assertThat(send("GET", "/notebooks/abc", WRITER, null, null).statusCode()).isEqualTo(400);
|
||||
|
||||
assertThat(json(send("PUT", "/notebooks/" + id, WRITER, "{\"title\":\"N2\"}", "application/json")).get("currentVersion")).isEqualTo(2);
|
||||
assertThat(json(send("POST", "/notebooks/" + id + "/publish", WRITER, "{\"shared\":true}", "application/json")).get("visibility")).isEqualTo("SHARED");
|
||||
assertThat((List<?>) json(send("GET", "/notebooks?scope=shared", READER, null, null)).get("notebooks")).hasSize(1);
|
||||
assertThat(json(send("POST", "/notebooks/" + id + "/duplicate", READER, "{}", "application/json")).get("ownerUserId")).isEqualTo(READER);
|
||||
assertThat((List<?>) json(send("GET", "/notebooks/" + id + "/versions", WRITER, null, null)).get("versions")).hasSize(2);
|
||||
assertThat(json(send("GET", "/notebooks/" + id + "/versions/1", WRITER, null, null)).get("versionNumber")).isEqualTo(1);
|
||||
assertThat(json(send("POST", "/notebooks/" + id + "/versions/1/restore", WRITER, "{}", "application/json")).get("title")).isEqualTo("N1");
|
||||
|
||||
String uuid = (String) json(send("POST", "/execute", WRITER, "{\"backendName\":\"h2\",\"sql\":\"SELECT 1 AS one\"}", "application/json")).get("executionUuid");
|
||||
Map<String, Object> snapshot = json(send("POST", "/notebooks/" + id + "/snapshots", WRITER, "{\"executionUuid\":\"" + uuid + "\",\"cellKey\":\"a\"}", "application/json"));
|
||||
assertThat((List<?>) json(send("GET", "/notebooks/" + id + "/snapshots", WRITER, null, null)).get("snapshots")).hasSize(1);
|
||||
assertThat(json(send("DELETE", "/notebooks/" + id + "/snapshots/" + snapshot.get("id"), WRITER, null, null)).get("deleted")).isEqualTo(true);
|
||||
|
||||
assertThat(send("DELETE", "/notebooks/" + id, READER, null, null).statusCode()).isEqualTo(403);
|
||||
assertThat(json(send("DELETE", "/notebooks/" + id, WRITER, null, null)).get("deleted")).isEqualTo(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Every spec checks access before parsing bodies, identifiers or formats.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEveryOperationDeniesMissingAccess() throws Exception
|
||||
{
|
||||
for(var reference : SqlWorkbenchEndpoints.extension().getSpecs())
|
||||
{
|
||||
var spec = (AbstractEndpointSpec<?, ?, ?>) Class.forName(reference.getName()).getConstructor().newInstance();
|
||||
var operation = spec.defineBasicOperation();
|
||||
String path = operation.getPath().substring("/sql-workbench".length()).replaceAll("\\{[^}]+}", "invalid");
|
||||
String method = operation.getHttpMethod().name();
|
||||
boolean body = method.equals("POST") || method.equals("PUT");
|
||||
HttpResponse<String> response = send(method, path + "?format=invalid", "nobody", body ? "not json" : null, body ? "text/plain" : null);
|
||||
assertThat(response.statusCode()).as(method + " " + path).isEqualTo(403);
|
||||
assertThat(json(response)).containsEntry("error", "Permission denied.");
|
||||
HttpRequest unauthenticated = HttpRequest.newBuilder(URI.create(base + path))
|
||||
.header("Authorization", "Bearer Deny")
|
||||
.method(method, HttpRequest.BodyPublishers.noBody()).build();
|
||||
assertThat(client.send(unauthenticated, HttpResponse.BodyHandlers.ofString()).statusCode()).as(method + " authentication").isEqualTo(401);
|
||||
}
|
||||
assertThat(json(send("GET", "/history", READER, null, null)).get("executions")).asList().isEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Write gates, read-only classification and concurrency limits remain audited.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testExecutionGatesAndLimits() throws Exception
|
||||
{
|
||||
String write = "{\"backendName\":\"h2\",\"sql\":\"DELETE FROM person\",\"mode\":\"WRITE\"}";
|
||||
assertThat(send("POST", "/execute", READER, write, "application/json").statusCode()).isEqualTo(403);
|
||||
config.setWriteModeEnabled(false);
|
||||
assertThat(send("POST", "/execute", WRITER, write, "application/json").statusCode()).isEqualTo(403);
|
||||
String analyze = "{\"backendName\":\"h2\",\"sql\":\"SELECT 1\",\"analyze\":true}";
|
||||
assertThat(send("POST", "/explain", WRITER, analyze, "application/json").statusCode()).isEqualTo(403);
|
||||
config.setWriteModeEnabled(true);
|
||||
assertThat(send("POST", "/explain", READER, analyze, "application/json").statusCode()).isEqualTo(403);
|
||||
assertThat(send("POST", "/execute", WRITER, write.replace(",\"mode\":\"WRITE\"", ""), "application/json").statusCode()).isEqualTo(400);
|
||||
|
||||
config.setMaxConcurrentExecutionsPerUser(1);
|
||||
var running = SqlWorkbenchRuntime.get(qInstance).getRunningStatements();
|
||||
String uuid = "3f2504e0-4f89-11d3-9a0c-0305e82c3302";
|
||||
var registration = running.register(uuid, READER, 1);
|
||||
try
|
||||
{
|
||||
HttpResponse<String> limited = send("POST", "/execute", READER, "{\"backendName\":\"h2\",\"sql\":\"SELECT 1\"}", "application/json");
|
||||
assertThat(limited.statusCode()).isEqualTo(400);
|
||||
assertThat(limited.body()).contains("executions running");
|
||||
assertThat(json(send("POST", "/executions/" + uuid + "/cancel", OTHER_READER, "{}", "application/json"))).containsEntry("cancelled", false);
|
||||
assertThat(registration.isCancelled()).isFalse();
|
||||
assertThat(json(send("POST", "/executions/" + uuid + "/cancel", READER, "{}", "application/json"))).containsEntry("cancelled", true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
running.unregister(uuid, registration);
|
||||
}
|
||||
List<?> history = (List<?>) json(send("GET", "/history", READER, null, null)).get("executions");
|
||||
assertThat(history).hasSize(3).allSatisfy(entry -> assertThat(((Map<?, ?>) entry).get("status")).isEqualTo("REJECTED"));
|
||||
Map<String, Object> count = json(send("POST", "/execute", READER, "{\"backendName\":\"h2\",\"sql\":\"SELECT COUNT(*) AS n FROM person\"}", "application/json"));
|
||||
assertThat(count.get("rows")).isEqualTo(List.of(List.of(5)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Share permission, ownership and cached-result ownership apply to mutations.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNotebookMutationDenials() throws Exception
|
||||
{
|
||||
Integer id = (Integer) json(send("POST", "/notebooks", READER, "{\"title\":\"Private\"}", "application/json")).get("id");
|
||||
assertThat(send("POST", "/notebooks/" + id + "/publish", READER, "{\"shared\":true}", "application/json").statusCode()).isEqualTo(403);
|
||||
for(String suffix : List.of("", "/versions", "/versions/1", "/snapshots"))
|
||||
{
|
||||
assertThat(send("GET", "/notebooks/" + id + suffix, OTHER_READER, null, null).statusCode()).isEqualTo(404);
|
||||
}
|
||||
assertThat(send("PUT", "/notebooks/" + id, OTHER_READER, "{\"title\":\"Stolen\"}", "application/json").statusCode()).isEqualTo(404);
|
||||
assertThat(send("POST", "/notebooks/" + id + "/duplicate", OTHER_READER, "{}", "application/json").statusCode()).isEqualTo(404);
|
||||
assertThat(send("POST", "/notebooks/" + id + "/versions/1/restore", OTHER_READER, "{}", "application/json").statusCode()).isEqualTo(404);
|
||||
|
||||
Integer shared = (Integer) json(send("POST", "/notebooks", WRITER, "{\"title\":\"Shared\"}", "application/json")).get("id");
|
||||
send("POST", "/notebooks/" + shared + "/publish", WRITER, "{\"shared\":true}", "application/json");
|
||||
assertThat(send("PUT", "/notebooks/" + shared, READER, "{\"title\":\"Stolen\"}", "application/json").statusCode()).isEqualTo(403);
|
||||
assertThat(send("POST", "/notebooks/" + shared + "/versions/1/restore", READER, "{}", "application/json").statusCode()).isEqualTo(403);
|
||||
String uuid = (String) json(send("POST", "/execute", WRITER, "{\"backendName\":\"h2\",\"sql\":\"SELECT 1\"}", "application/json")).get("executionUuid");
|
||||
String snapshotBody = "{\"executionUuid\":\"" + uuid + "\",\"cellKey\":\"a\"}";
|
||||
assertThat(send("POST", "/notebooks/" + id + "/snapshots", READER, snapshotBody, "application/json").statusCode()).isEqualTo(404);
|
||||
Integer snapshotId = (Integer) json(send("POST", "/notebooks/" + shared + "/snapshots", WRITER, snapshotBody, "application/json")).get("id");
|
||||
assertThat(send("DELETE", "/notebooks/" + shared + "/snapshots/" + snapshotId, READER, null, null).statusCode()).isEqualTo(403);
|
||||
assertThat(json(send("GET", "/notebooks/" + id, READER, null, null))).containsEntry("title", "Private");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Keep decimal parameters, null bodies, SQL failures and all export formats.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testWireCompatibility() throws Exception
|
||||
{
|
||||
HttpResponse<String> run = send("POST", "/execute", READER,
|
||||
"{\"backendName\":\"h2\",\"sql\":\"SELECT CAST(:n AS DECIMAL(30,20)) AS n\",\"params\":{\"n\":0.12345678901234567890}}", "application/json");
|
||||
assertThat(run.statusCode()).isEqualTo(200);
|
||||
assertThat(run.body()).contains("0.12345678901234567890");
|
||||
String uuid = (String) json(run).get("executionUuid");
|
||||
for(String format : List.of("csv", "json", "xlsx"))
|
||||
{
|
||||
HttpResponse<String> export = send("GET", "/executions/" + uuid + "/export?format=" + format, READER, null, null);
|
||||
assertThat(export.statusCode()).isEqualTo(200);
|
||||
assertThat(export.headers().firstValue("Cache-Control")).contains("no-store");
|
||||
assertThat(export.headers().firstValue("Content-Disposition").orElseThrow()).endsWith("." + format + "\"");
|
||||
if(format.equals("json"))
|
||||
{
|
||||
assertThat(export.body()).contains("0.12345678901234567890");
|
||||
}
|
||||
}
|
||||
assertThat(json(send("POST", "/executions/" + uuid + "/cancel", READER, "null", "application/json"))).containsEntry("cancelled", false);
|
||||
assertThat(send("POST", "/execute", READER, "[]", "application/json").statusCode()).isEqualTo(400);
|
||||
assertThat(send("POST", "/execute", READER, "{}", null).statusCode()).isEqualTo(400);
|
||||
HttpResponse<String> failed = send("POST", "/execute", READER, "{\"backendName\":\"h2\",\"sql\":\"SELECT * FROM nonexistent_table\"}", "application/json");
|
||||
assertThat(failed.statusCode()).isEqualTo(200);
|
||||
assertThat(json(failed)).containsEntry("status", "FAILED");
|
||||
String served = client.send(HttpRequest.newBuilder(URI.create(base.replace("/sql-workbench", "/openapi.yaml"))).build(), HttpResponse.BodyHandlers.ofString()).body();
|
||||
assertThat(served).contains("sqlWorkbenchExecute", "sqlWorkbenchExportResult", "SqlWorkbenchNotebookV1");
|
||||
}
|
||||
|
||||
}
|
||||
-203
@@ -1,203 +0,0 @@
|
||||
/*
|
||||
* 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.api;
|
||||
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qbits.sqlworkbench.BaseSqlWorkbenchTest;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinMetaData;
|
||||
import io.javalin.Javalin;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The routes over real HTTP (Javalin on a random port, MOCK authentication
|
||||
** with the sessionId cookie choosing the test user).
|
||||
*******************************************************************************/
|
||||
class SqlWorkbenchRouteProviderTest extends BaseSqlWorkbenchTest
|
||||
{
|
||||
private Javalin javalin;
|
||||
private String base;
|
||||
private HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void startServer()
|
||||
{
|
||||
QContext.clear();
|
||||
SqlWorkbenchRouteProvider provider = new SqlWorkbenchRouteProvider();
|
||||
provider.setQInstance(qInstance);
|
||||
javalin = Javalin.create(provider::acceptJavalinConfig).start(0);
|
||||
base = "http://localhost:" + javalin.port() + SqlWorkbenchRouteProvider.BASE_PATH;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void stopServer()
|
||||
{
|
||||
javalin.stop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private HttpResponse<String> send(String method, String path, String user, String body, String contentType) throws Exception
|
||||
{
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(base + path)).header("Cookie", "sessionId=" + user);
|
||||
if(contentType != null)
|
||||
{
|
||||
builder.header("Content-Type", contentType);
|
||||
}
|
||||
builder.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(body));
|
||||
return (client.send(builder.build(), HttpResponse.BodyHandlers.ofString()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Map<String, Object> json(HttpResponse<String> response) throws Exception
|
||||
{
|
||||
return (JsonUtils.toObject(response.body(), new TypeReference<Map<String, Object>>() {}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRegistration()
|
||||
{
|
||||
com.kingsrook.qbits.sqlworkbench.api.SqlWorkbenchRoutes.register(qInstance);
|
||||
List<QCodeReference> references = QJavalinMetaData.of(qInstance).getAdditionalRouteProviderReferences();
|
||||
assertThat(references).filteredOn(r -> r.getName().equals(SqlWorkbenchRouteProvider.class.getName())).hasSize(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testEndToEnd() throws Exception
|
||||
{
|
||||
assertThat(send("GET", "/capabilities", "nobody", null, null).statusCode()).isEqualTo(403);
|
||||
assertThat(send("GET", "/capabilities", READER, null, null).statusCode()).isEqualTo(200);
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// JSON only - a form post (CSRF shape) is refused //
|
||||
////////////////////////////////////////////////////////////
|
||||
assertThat(send("POST", "/execute", READER, "backendName=h2&sql=SELECT+1", "application/x-www-form-urlencoded").statusCode()).isEqualTo(400);
|
||||
assertThat(send("POST", "/execute", READER, "not json", "application/json").statusCode()).isEqualTo(400);
|
||||
|
||||
HttpResponse<String> run = send("POST", "/execute", READER, "{\"backendName\":\"h2\",\"sql\":\"SELECT id, salary FROM person WHERE salary > :s ORDER BY id\",\"params\":{\"s\":25000.25}}", "application/json");
|
||||
assertThat(run.statusCode()).isEqualTo(200);
|
||||
Map<String, Object> result = json(run);
|
||||
assertThat(result.get("status")).isEqualTo("SUCCEEDED");
|
||||
assertThat(result.get("rowCount")).isEqualTo(2);
|
||||
String uuid = (String) result.get("executionUuid");
|
||||
|
||||
assertThat(json(send("GET", "/executions/" + uuid + "/rows?offset=1&limit=1&sort=0&dir=desc", READER, null, null)).get("rows")).asList().hasSize(1);
|
||||
assertThat(send("GET", "/executions/" + uuid + "/rows?offset=x", READER, null, null).statusCode()).isEqualTo(400);
|
||||
assertThat(send("GET", "/executions/" + uuid + "/rows", OTHER_READER, null, null).statusCode()).isEqualTo(404);
|
||||
|
||||
HttpResponse<String> csv = send("GET", "/executions/" + uuid + "/export?format=csv", READER, null, null);
|
||||
assertThat(csv.statusCode()).isEqualTo(200);
|
||||
assertThat(csv.headers().firstValue("Content-Disposition").orElse("")).contains("attachment").contains(".csv");
|
||||
assertThat(csv.body()).contains("id,salary");
|
||||
assertThat(send("GET", "/executions/" + uuid + "/export?format=pdf", READER, null, null).statusCode()).isEqualTo(400);
|
||||
assertThat(send("GET", "/executions/" + uuid + "/export", OTHER_READER, null, null).statusCode()).isEqualTo(404);
|
||||
|
||||
assertThat(send("POST", "/execute", READER, "{\"backendName\":\"h2\",\"sql\":\"DELETE FROM person\",\"mode\":\"WRITE\"}", "application/json").statusCode()).isEqualTo(403);
|
||||
assertThat(send("POST", "/execute", READER, "{\"backendName\":\"nope\",\"sql\":\"SELECT 1\"}", "application/json").statusCode()).isEqualTo(404);
|
||||
assertThat(json(send("POST", "/explain", READER, "{\"backendName\":\"h2\",\"sql\":\"SELECT 1\"}", "application/json")).get("status")).isEqualTo("SUCCEEDED");
|
||||
assertThat(json(send("POST", "/executions/" + uuid + "/cancel", READER, "{}", "application/json")).get("cancelled")).isEqualTo(false);
|
||||
assertThat((List<?>) json(send("GET", "/history?limit=5", READER, null, null)).get("executions")).hasSize(3);
|
||||
assertThat(json(send("GET", "/schema/h2", READER, null, null)).get("dialect")).isEqualTo("h2");
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// a client-chosen id: any case on the paths, single-use (409) //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
String clientUuid = "3F2504E0-4F89-11D3-9A0C-0305E82C3301";
|
||||
String body = "{\"backendName\":\"h2\",\"sql\":\"SELECT 1 AS one\",\"executionUuid\":\"" + clientUuid + "\"}";
|
||||
assertThat(json(send("POST", "/execute", READER, body, "application/json")).get("executionUuid")).isEqualTo(clientUuid.toLowerCase());
|
||||
assertThat(send("GET", "/executions/" + clientUuid + "/rows", READER, null, null).statusCode()).isEqualTo(200);
|
||||
assertThat(send("GET", "/executions/" + clientUuid + "/export", READER, null, null).statusCode()).isEqualTo(200);
|
||||
HttpResponse<String> again = send("POST", "/execute", READER, body, "application/json");
|
||||
assertThat(again.statusCode()).isEqualTo(409);
|
||||
assertThat(again.body()).contains("already used");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNotebookRoutes() throws Exception
|
||||
{
|
||||
Map<String, Object> created = json(send("POST", "/notebooks", WRITER, "{\"title\":\"N1\",\"cells\":[{\"cellKey\":\"a\",\"cellType\":\"SQL\",\"content\":\"SELECT 1\"}]}", "application/json"));
|
||||
Integer id = (Integer) created.get("id");
|
||||
|
||||
assertThat((List<?>) json(send("GET", "/notebooks?scope=mine", WRITER, null, null)).get("notebooks")).hasSize(1);
|
||||
assertThat(json(send("GET", "/notebooks/" + id, WRITER, null, null)).get("title")).isEqualTo("N1");
|
||||
assertThat(send("GET", "/notebooks/" + id, READER, null, null).statusCode()).isEqualTo(404);
|
||||
assertThat(send("GET", "/notebooks/abc", WRITER, null, null).statusCode()).isEqualTo(400);
|
||||
|
||||
assertThat(json(send("PUT", "/notebooks/" + id, WRITER, "{\"title\":\"N2\"}", "application/json")).get("currentVersion")).isEqualTo(2);
|
||||
assertThat(json(send("POST", "/notebooks/" + id + "/publish", WRITER, "{\"shared\":true}", "application/json")).get("visibility")).isEqualTo("SHARED");
|
||||
assertThat((List<?>) json(send("GET", "/notebooks?scope=shared", READER, null, null)).get("notebooks")).hasSize(1);
|
||||
assertThat(json(send("POST", "/notebooks/" + id + "/duplicate", READER, "{}", "application/json")).get("ownerUserId")).isEqualTo(READER);
|
||||
assertThat((List<?>) json(send("GET", "/notebooks/" + id + "/versions", WRITER, null, null)).get("versions")).hasSize(2);
|
||||
assertThat(json(send("GET", "/notebooks/" + id + "/versions/1", WRITER, null, null)).get("versionNumber")).isEqualTo(1);
|
||||
assertThat(json(send("POST", "/notebooks/" + id + "/versions/1/restore", WRITER, "{}", "application/json")).get("title")).isEqualTo("N1");
|
||||
|
||||
String uuid = (String) json(send("POST", "/execute", WRITER, "{\"backendName\":\"h2\",\"sql\":\"SELECT 1 AS one\"}", "application/json")).get("executionUuid");
|
||||
Map<String, Object> snapshot = json(send("POST", "/notebooks/" + id + "/snapshots", WRITER, "{\"executionUuid\":\"" + uuid + "\",\"cellKey\":\"a\"}", "application/json"));
|
||||
assertThat((List<?>) json(send("GET", "/notebooks/" + id + "/snapshots", WRITER, null, null)).get("snapshots")).hasSize(1);
|
||||
assertThat(json(send("DELETE", "/notebooks/" + id + "/snapshots/" + snapshot.get("id"), WRITER, null, null)).get("deleted")).isEqualTo(true);
|
||||
|
||||
assertThat(send("DELETE", "/notebooks/" + id, READER, null, null).statusCode()).isEqualTo(403);
|
||||
assertThat(json(send("DELETE", "/notebooks/" + id, WRITER, null, null)).get("deleted")).isEqualTo(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
+35
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user