Skip to content

feat(connectors): database selector + per-card write toggle (#568) - #631

Closed
alfredo1996 wants to merge 16 commits into
release/2.0from
feat/issue-568-db-selector-write-toggle
Closed

feat(connectors): database selector + per-card write toggle (#568)#631
alfredo1996 wants to merge 16 commits into
release/2.0from
feat/issue-568-db-selector-write-toggle

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Apr 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Database introspection: listDatabases() for Neo4j (SHOW DATABASES, graceful fallback for < 4.x) and PostgreSQL (pg_database + information_schema.schemata for schemas)
  • Per-card database selector: Widget editor shows a database dropdown (Data tab) when the connection allows per-card DB selection (allow_per_card_db, default true)
  • Per-card write toggle: "Enable write mode" checkbox in the Advanced tab, visible only for users with canWrite=true. Server enforces by looking up the widget in the dashboard layout
  • API routes: GET /connections/[id]/databases and POST /connections/list-databases-inline for fetching available databases
  • Migration: Adds allow_per_card_db column to connections table

Screenshots

Data Tab (clean) Advanced Tab — Write Mode OFF Advanced Tab — Write Mode ON
data-tab write-off write-on

Test plan

  • Unit: listDatabases for Neo4j + PostgreSQL via Testcontainers (10 tests)
  • Unit: Database listing API routes — auth, validation, success/failure (11 tests)
  • Unit: Write route — allowWrites enforcement + legacy form-widget path (15 tests)
  • Unit: All 1811 app tests pass
  • Unit: All 1238 component tests pass
  • E2E: 271 passed, 0 failed (including write-permissions suite)
  • Build: Production build passes

Closes #568

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Per-widget database selection (choose connection default or a specific database) when the connection allows it.
    • Per-widget "Write Mode" toggle to enable/disable widget-level writes (shown conditionally to authorized users).
    • Query execution supports an optional per-widget database override.
  • Tests

    • Expanded coverage for database/schema listing, per-widget DB overrides, and write-permission/authorization flows.

dependabot Bot and others added 9 commits April 13, 2026 05:27
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](docker/metadata-action@v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…cker/metadata-action-6

chore(deps): bump docker/metadata-action from 5 to 6
…cker/login-action-4

chore(deps): bump docker/login-action from 3 to 4
…ftprops/action-gh-release-3

chore(deps): bump softprops/action-gh-release from 2 to 3
…egatives and hyphens (#581)

Adds DashboardErrorBoundary wrapping the dashboard page content so a
crash in ParameterBar/DashboardGrid/CrossFilterTag shows recovery UI
instead of white-screening the app.

Fixes safeEvaluateExpression tokenizer to handle negative numeric
literals (-5, -3.14) and hyphenated column names (revenue-total) via
a regex-based tokenizer with unary minus disambiguation.

Closes #576

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…oute (#579)

The ownership check at line 47-53 filtered by connectionId + userId but
omitted tenantId, allowing a theoretical cross-tenant query if user IDs
collided. The admin and dashboard-access fallbacks already included it.

Closes #572

Co-authored-by: alfredorubin96 <alfredo.rubin@neotechnology.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add database introspection to connectors (listDatabases for Neo4j/PG,
listSchemas for PG) with graceful fallback. Introduce per-widget write
mode toggle in Advanced tab and per-card database override in widget
editor. Server enforces allowWrites by looking up the widget in the
dashboard layout before executing write queries.

- connection/: abstract listDatabases(), Neo4j SHOW DATABASES with
  fallback, PG pg_database + information_schema.schemata
- API: GET /connections/[id]/databases, POST /connections/list-databases-inline
- Schema: allow_per_card_db on connections, database + allowWrites on DashboardWidget
- Write route: when widgetId + dashboardId provided, verifies widget.allowWrites;
  legacy form-widget path (no widgetId) still works with user-level canWrite
- Widget editor: database dropdown (Data tab), write mode toggle (Advanced tab)
- 26 new tests (10 connector integration, 11 API route, 5 write enforcement)

Closes #568

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@alfredo1996 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 38 minutes and 19 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8f5c8399-ca2a-4fdf-aceb-139e50d6c874

📥 Commits

Reviewing files that changed from the base of the PR and between fe45b90 and f7df09c.

📒 Files selected for processing (1)
  • app/src/app/api/connections/list-databases-inline/route.ts

Walkthrough

Adds per-card database selection and per-card write-mode controls. Implements connector APIs to list databases/schemas, new endpoints to fetch databases (saved and inline), threads database override through query execution and caching, extends widget editor/store/UI with database + allowWrites, and enforces widget-level write authorization server-side.

Changes

Cohort / File(s) Summary
Database Enumeration API
app/src/app/api/connections/[id]/databases/route.ts, app/src/app/api/connections/[id]/databases/__tests__/route.test.ts, app/src/app/api/connections/list-databases-inline/route.ts, app/src/app/api/connections/list-databases-inline/__tests__/route.test.ts
Adds GET/POST endpoints to list databases (and schemas for Postgres). Decrypts credentials, calls executor helpers listDatabases/listSchemas, returns arrays; includes auth/validation and failure-path tests.
Query Routes (per-card DB & write gating)
app/src/app/api/query/route.ts, app/src/app/api/query/write/route.ts, app/src/app/api/query/write/__tests__/route.test.ts, app/src/app/api/connections/route.ts
Accepts optional database in query requests; write route can resolve widget/dashboard, enforces widget.allowWrites and connection allowPerCardDb, and applies per-card DB override to executed credentials. Tests updated for tenant scoping and new flows.
Widget Editor UI & Page
app/src/app/(dashboard)/[id]/edit/page.tsx, app/src/components/widget-editor-modal.tsx, app/src/components/widget-editor/database-selector.tsx, app/src/components/card-container.tsx
Adds DatabaseSelector component; editor gains canWrite prop and write-toggle UI; widget database included in query input for cache keying.
Hooks & Store
app/src/hooks/use-connection-databases.ts, app/src/hooks/use-connections.ts, app/src/hooks/use-query-execution.ts, app/src/hooks/use-widget-query.ts, app/src/stores/widget-editor-store.ts
New useConnectionDatabases hook; ConnectionListItem gains allowPerCardDb; query hooks and widget query types accept optional database; widget-editor store adds database and allowWrites state and setters.
DB Schema / Types
app/src/lib/db/schema.ts
Adds connections.allow_per_card_db column (default true) and augments DashboardWidget with database?: string and allowWrites?: boolean.
Query Executor & Connectors
app/src/lib/query/query-executor.ts, connection/src/generalized/ConnectionModule.ts, connection/src/neo4j/Neo4jConnectionModule.ts, connection/src/postgresql/PostgresConnectionModule.ts, connection/__tests__/connection/list-databases.ts
Introduces listDatabases and listSchemas executor helpers; extends connector interface and implements listing for Neo4j/Postgres with failure fallbacks; adds connector tests (including testcontainers).
Tests / Component tests
app/src/components/widget-editor/__tests__/database-selector.test.tsx, app/src/hooks/__tests__/use-connection-databases.test.ts, app/src/lib/__tests__/query/query-executor-core.test.ts, app/src/stores/__tests__/widget-editor-store.test.ts
New unit tests for DatabaseSelector, useConnectionDatabases, executor list functions, and widget-editor store behaviors.

Sequence Diagrams

sequenceDiagram
    participant User as Widget Editor
    participant API as GET /api/connections/[id]/databases
    participant DB as Database Store
    participant Connector as Connection Module
    User->>API: Request databases for connectionId
    activate API
    API->>DB: Fetch connection record & encrypted config
    activate DB
    DB-->>API: connection + encrypted config
    deactivate DB
    API->>Connector: listDatabases(type, decryptedCredentials)
    activate Connector
    Connector->>Connector: Query system catalogs (SHOW DATABASES / pg_database)
    Connector-->>API: string[] database names
    deactivate Connector
    API-->>User: { data: { databases: [...] } }
    deactivate API
Loading
sequenceDiagram
    participant Client as Client
    participant WriteAPI as POST /api/query/write
    participant Lookup as Dashboard Lookup
    participant DB as Database Store
    participant Executor as Query Executor
    Client->>WriteAPI: Execute write (widgetId, dashboardId, connectionId, query)
    activate WriteAPI
    WriteAPI->>Lookup: Load dashboard by id + tenant
    activate Lookup
    Lookup->>DB: SELECT dashboard WHERE id=?
    activate DB
    DB-->>Lookup: dashboard record (widgets)
    deactivate DB
    Lookup-->>WriteAPI: widget.allowWrites, widget.database, widget.connectionId
    deactivate Lookup
    alt widget not found or mismatch
        WriteAPI-->>Client: 404 or 403
    else if widget.allowWrites = false
        WriteAPI-->>Client: 403 Forbidden
    else
        alt connection.allowPerCardDb = true and widget.database set
            WriteAPI->>Executor: executeQuery(effectiveCredentials with widget.database)
        else
            WriteAPI->>Executor: executeQuery(original credentials)
        end
        activate Executor
        Executor-->>WriteAPI: result
        deactivate Executor
        WriteAPI-->>Client: 200 OK with result
    end
    deactivate WriteAPI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, pkg:app, pkg:connection, area:connectors, area:widgets, testing

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: database selector UI + per-card write toggle feature for connectors.
Linked Issues check ✅ Passed All coding requirements from #568 are implemented: listDatabases for Neo4j/PostgreSQL, API routes, database selector UI, per-card write toggle, server-side enforcement, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes directly support the PR objectives: connector introspection APIs, database selector component, per-card write controls, schema updates, and related tests—no extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-568-db-selector-write-toggle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 38 minutes and 19 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/src/hooks/use-widget-query.ts (1)

10-16: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Thread database into the cache key.

This hook now accepts a per-card database override, but useQuery still caches only by connection/query/params. Two cards on the same connection but different databases will reuse the same cached payload and skip a refetch.

♻️ Proposed fix
   queryKey: [
     "widget-query",
     mergedInput?.connectionId,
+    mergedInput?.database,
     mergedInput?.query,
     mergedInput?.params,
     options?.staleTime ?? 0,
   ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/hooks/use-widget-query.ts` around lines 10 - 16, The hook accepts a
per-card database override (WidgetQueryInput.database) but the cache key passed
to useQuery omits it, causing cross-database cache collisions; update the cache
key used in useWidgetQuery/useQuery to include the database (e.g., add database
as an element in the key array or include it in the serialized params) and
ensure you handle undefined consistently (use null or empty string) so two cards
on the same connection with different database values get distinct cache entries
and trigger separate fetches.
app/src/app/api/connections/route.ts (1)

58-77: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return allowPerCardDb from create as well.

The list endpoint now exposes the new flag, but the POST response still returns a partial row. That leaves useCreateConnection() with allowPerCardDb === undefined for freshly created connections until a refetch happens.

♻️ Proposed fix
       .returning({
         id: connections.id,
         name: connections.name,
         type: connections.type,
+        allowPerCardDb: connections.allowPerCardDb,
         createdAt: connections.createdAt,
+        updatedAt: connections.updatedAt,
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/connections/route.ts` around lines 58 - 77, The POST response
omits the allowPerCardDb flag so newly created connections come back with
allowPerCardDb === undefined; update the insert/returning to include
connections.allowPerCardDb and ensure the values object includes allowPerCardDb
when present (so the inserted row contains that flag), e.g. add allowPerCardDb
to the .values({ ... }) payload and to the .returning({ id: connections.id,
name: connections.name, type: connections.type, allowPerCardDb:
connections.allowPerCardDb, createdAt: connections.createdAt }), leaving
prefetchSchema(type, result.data.config) and apiSuccess(connection, 201) logic
unchanged.
app/src/app/api/query/write/__tests__/route.test.ts (1)

176-239: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cover the new per-card database path here.

These success cases still only assert accessMode: "WRITE". Add an assertion that the widget-scoped database from the dashboard lookup reaches the executor, otherwise the new database selector can regress without failing tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/query/write/__tests__/route.test.ts` around lines 176 - 239,
The tests call out write execution but don't assert the per-card (widget-scoped)
database is propagated; update both success tests to also assert
mockExecuteQuery received the widget-scoped database from the dashboard lookup:
after invoking POST in each test, add an expectation that mockExecuteQuery was
called with the connection object (the second argument) containing the database
property equal to the widget-level DB value returned by
mockConnectionAndDashboard (use the same mockConnectionAndDashboard/setup that
provides the widget DB), e.g.
expect(mockExecuteQuery).toHaveBeenCalledWith(expect.any(String),
expect.objectContaining({ database: "EXPECTED_WIDGET_DB" }), expect.any(Object),
expect.objectContaining({ accessMode: "WRITE" })); reference
mockConnectionAndDashboard, mockExecuteQuery, makeRequest, POST, and
writerSession to locate where to add this assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/app/api/connections/list-databases-inline/route.ts`:
- Around line 20-25: The route is constructing a partial credentials object
(credentials) and only forwarding uri/username/password/database to
testInlineSchema, which omits SSL, timeout, and pool settings and breaks some
connections; update the route (where credentials is built) to forward the entire
validated connection config (include sslRejectUnauthorized, any ssl fields,
statement/query timeouts, pool settings, and any other properties present on
config) to testInlineSchema and any callers rather than a trimmed object so
testInlineSchema receives the full validated config it expects.

In `@app/src/app/api/query/route.ts`:
- Around line 24-25: The schema's caller-supplied "database" override must not
be trusted; stop using the raw database field from the request body to select
DBs. Instead, in the route handler that reads the zod schema (the "database"
field) enforce that database is derived only after verifying the
widget/card/dashboard ownership or access (perform the saved widget/dashboard
lookup and resolve the DB name from that record), or allow the override only
when the request is in a trusted edit flow (check the user's edit permissions or
an explicit server-side flag) and disallow it for dashboard-access fallback
viewers; replace any direct use of the body.database with the verified source
and remove fallback logic that accepts arbitrary caller-supplied DB names when
allowPerCardDb is true.

In `@app/src/app/api/query/write/route.ts`:
- Around line 22-25: The schema currently makes widgetId and dashboardId
optional allowing callers to omit them and bypass widget write scoping; update
the request validation so widget-originated writes require explicit widget
context or a server-validated discriminator: either make widgetId and
dashboardId required (remove .optional()) for widget paths, or add a required
discriminant (e.g. source: z.enum(['widget','legacy']) or isWidget: z.boolean())
and enforce via Zod refinements/preconditions that when source==='widget' the
widgetId and dashboardId (z.string().min(1)) must be present; then in the route
handler use that discriminant to route to the legacy branch and ensure
allowWrites is checked server-side (wherever checkAllowWrites/allowWrites logic
is invoked).
- Around line 81-104: Ensure the resolved widget is actually bound to this
connection and respect the connection-level per-card DB policy before
authorizing the write: after locating widget (variable widget) assert
widget.connectionId === connectionId (reject if not) and enforce server-side
can_write authorization for the connection (invoke the same permission check
used elsewhere for connection writes); only apply widget.database to override
credentials when the connection object grants allowPerCardDb === true (otherwise
ignore widgetDatabaseOverride) so effectiveCredentials are computed from
decryptJson(connection.configEncrypted) plus widget.database only when both
widget belongs to this connection and connection.allowPerCardDb is true.

In `@app/src/components/widget-editor-modal.tsx`:
- Around line 1496-1522: The Write Mode checkbox is shown even for content-only
widgets (markdown/iframe) and can store misleading write metadata; update the
conditional around the Checkbox (currently using canWrite && !isLabMode) in the
WidgetEditorModal component to also check the widget type (e.g., add a guard
like !isContentOnly or explicit checks for widget.type !== 'markdown' &&
widget.type !== 'iframe') before rendering the Checkbox/Label and before calling
setAllowWrites, ensuring allowWrites and setAllowWrites are only presented for
widgets that can actually execute write queries.
- Around line 952-953: When assembling the widget payload in
widget-editor-modal.tsx, don't persist a previously chosen database override by
unconditionally writing database: database || undefined (and similarly
allowWrites); instead only include the database and allowWrites fields when the
per-card DB selection UI is visible (e.g., a flag such as
isDatabaseSelectionVisible or connectionAllowsPerCardDb) and the values are
explicitly set—otherwise omit those keys or explicitly set them to undefined to
clear any stale override in the widget payload.

In `@app/src/components/widget-editor/database-selector.tsx`:
- Around line 36-37: The early return when databases.length === 0 causes the DB
picker to vanish; remove that return and ensure the DatabaseSelector component
still renders when the databases array is empty by rendering the selector UI
with a fallback option/placeholder (e.g., a disabled "<No databases available>"
option or a default-selection control) and keeping any existing selection logic
intact; update the render path in the component that references the databases
variable so it displays the empty-state option instead of returning null.

In `@app/src/lib/query/query-executor.ts`:
- Around line 217-225: The listDatabases flow can reuse a cached module that was
created with different credentials because getOrCreateModule's cache key omits
credentials.password; update the cache-key logic used by getOrCreateModule so it
includes credentials.password (or otherwise incorporate a credentials-derived
fingerprint) so modules are keyed per exact ConnectionCredentials, and ensure
listDatabases (and other helpers like any credential probe wrappers) call
getOrCreateModule with the same credentials object; locate getOrCreateModule and
the cache/key generation logic and add the password (or secure hash of it) to
the key to prevent reusing modules authenticated with an old password.

In `@app/src/stores/widget-editor-store.ts`:
- Around line 278-280: setConnectionId currently only updates connectionId and
leaves the connection-scoped database override intact; change setConnectionId to
also clear the database override when switching connections. Update the setter
implementation for setConnectionId (the store action named setConnectionId) to
call set({ connectionId: id, database: undefined }) (or null) so the previous
connection's database value is cleared whenever connectionId changes.

In `@connection/__tests__/connection/list-databases.ts`:
- Around line 43-55: The test currently exercises the happy path; modify it to
force the fallback by mocking the Neo4j driver/SHOW DATABASES call to throw so
listDatabases() hits its catch path: instantiate Neo4jConnectionModule as
before, stub or mock the internal driver/session method used by
Neo4jConnectionModule.listDatabases() to throw an error (or return a rejected
promise), then call module.listDatabases() and assert it resolves to an empty
array (expect(databases).toEqual([]) or Array.isArray check), finally restore
the mock and call module.close(); target the listDatabases() method and the
driver/session call it uses when setting up the mock.

In `@connection/src/postgresql/PostgresConnectionModule.ts`:
- Around line 238-254: In listSchemas(), the WHERE clause is too broad and
misses excluding information_schema while also mis-using '_' as a wildcard;
update the SQL to explicitly exclude information_schema and treat the literal
underscore correctly, e.g. change the query in
PostgresConnectionModule.listSchemas to use "WHERE schema_name <>
'information_schema' AND schema_name NOT LIKE 'pg\\_%' ESCAPE '\\' ORDER BY
schema_name" so it excludes pg_* system schemas (with a literal underscore) and
information_schema but does not accidentally drop other user schemas.

---

Outside diff comments:
In `@app/src/app/api/connections/route.ts`:
- Around line 58-77: The POST response omits the allowPerCardDb flag so newly
created connections come back with allowPerCardDb === undefined; update the
insert/returning to include connections.allowPerCardDb and ensure the values
object includes allowPerCardDb when present (so the inserted row contains that
flag), e.g. add allowPerCardDb to the .values({ ... }) payload and to the
.returning({ id: connections.id, name: connections.name, type: connections.type,
allowPerCardDb: connections.allowPerCardDb, createdAt: connections.createdAt }),
leaving prefetchSchema(type, result.data.config) and apiSuccess(connection, 201)
logic unchanged.

In `@app/src/app/api/query/write/__tests__/route.test.ts`:
- Around line 176-239: The tests call out write execution but don't assert the
per-card (widget-scoped) database is propagated; update both success tests to
also assert mockExecuteQuery received the widget-scoped database from the
dashboard lookup: after invoking POST in each test, add an expectation that
mockExecuteQuery was called with the connection object (the second argument)
containing the database property equal to the widget-level DB value returned by
mockConnectionAndDashboard (use the same mockConnectionAndDashboard/setup that
provides the widget DB), e.g.
expect(mockExecuteQuery).toHaveBeenCalledWith(expect.any(String),
expect.objectContaining({ database: "EXPECTED_WIDGET_DB" }), expect.any(Object),
expect.objectContaining({ accessMode: "WRITE" })); reference
mockConnectionAndDashboard, mockExecuteQuery, makeRequest, POST, and
writerSession to locate where to add this assertion.

In `@app/src/hooks/use-widget-query.ts`:
- Around line 10-16: The hook accepts a per-card database override
(WidgetQueryInput.database) but the cache key passed to useQuery omits it,
causing cross-database cache collisions; update the cache key used in
useWidgetQuery/useQuery to include the database (e.g., add database as an
element in the key array or include it in the serialized params) and ensure you
handle undefined consistently (use null or empty string) so two cards on the
same connection with different database values get distinct cache entries and
trigger separate fetches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19b4ff91-e01d-43ee-a948-999f2846e6ce

📥 Commits

Reviewing files that changed from the base of the PR and between 14f91a5 and 3af0545.

⛔ Files ignored due to path filters (9)
  • app/drizzle/migrations/0003_furry_scourge.sql is excluded by !app/drizzle/migrations/**
  • app/drizzle/migrations/meta/0003_snapshot.json is excluded by !app/drizzle/migrations/**
  • app/drizzle/migrations/meta/_journal.json is excluded by !app/drizzle/migrations/**
  • screenshots/01-login.png is excluded by !**/*.png
  • screenshots/02-dashboards-home.png is excluded by !**/*.png
  • screenshots/03-dashboard-edit.png is excluded by !**/*.png
  • screenshots/04-widget-editor-data-tab.png is excluded by !**/*.png
  • screenshots/05-advanced-tab-write-mode-off.png is excluded by !**/*.png
  • screenshots/06-advanced-tab-write-mode-on.png is excluded by !**/*.png
📒 Files selected for processing (23)
  • app/src/app/(dashboard)/[id]/edit/page.tsx
  • app/src/app/api/connections/[id]/databases/__tests__/route.test.ts
  • app/src/app/api/connections/[id]/databases/route.ts
  • app/src/app/api/connections/list-databases-inline/__tests__/route.test.ts
  • app/src/app/api/connections/list-databases-inline/route.ts
  • app/src/app/api/connections/route.ts
  • app/src/app/api/query/route.ts
  • app/src/app/api/query/write/__tests__/route.test.ts
  • app/src/app/api/query/write/route.ts
  • app/src/components/card-container.tsx
  • app/src/components/widget-editor-modal.tsx
  • app/src/components/widget-editor/database-selector.tsx
  • app/src/hooks/use-connection-databases.ts
  • app/src/hooks/use-connections.ts
  • app/src/hooks/use-query-execution.ts
  • app/src/hooks/use-widget-query.ts
  • app/src/lib/db/schema.ts
  • app/src/lib/query/query-executor.ts
  • app/src/stores/widget-editor-store.ts
  • connection/__tests__/connection/list-databases.ts
  • connection/src/generalized/ConnectionModule.ts
  • connection/src/neo4j/Neo4jConnectionModule.ts
  • connection/src/postgresql/PostgresConnectionModule.ts

Comment thread app/src/app/api/connections/list-databases-inline/route.ts Outdated
Comment on lines +24 to +25
/** Per-card database override — used when the connection allows per-card DB selection. */
database: z.string().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't trust a caller-supplied database override here.

This now lets any caller who can execute the connection switch to an arbitrary database as long as allowPerCardDb is true. Because the route also has the dashboard-access fallback, a shared viewer can tamper with the body and read from databases that were never selected on the saved card. Derive the database from a verified widget/dashboard lookup, or restrict overrides to trusted edit flows only.

Also applies to: 106-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/query/route.ts` around lines 24 - 25, The schema's
caller-supplied "database" override must not be trusted; stop using the raw
database field from the request body to select DBs. Instead, in the route
handler that reads the zod schema (the "database" field) enforce that database
is derived only after verifying the widget/card/dashboard ownership or access
(perform the saved widget/dashboard lookup and resolve the DB name from that
record), or allow the override only when the request is in a trusted edit flow
(check the user's edit permissions or an explicit server-side flag) and disallow
it for dashboard-access fallback viewers; replace any direct use of the
body.database with the verified source and remove fallback logic that accepts
arbitrary caller-supplied DB names when allowPerCardDb is true.

Comment on lines +22 to +25
/** Widget ID — required so the server can verify allowWrites on the widget. */
widgetId: z.string().min(1).optional(),
/** Dashboard ID — required alongside widgetId for lookup. */
dashboardId: z.string().min(1).optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Make widget scoping explicit instead of optional.

Any caller can drop widgetId/dashboardId and hit the legacy branch, so allowWrites is not actually enforced for dashboard widgets unless the client cooperates. Keep the legacy form path behind an explicit server-validated discriminator, or require widget context for all widget-originated writes. As per coding guidelines "ALWAYS enforce can_write permission server-side in API routes, not just in UI."

Also applies to: 60-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/query/write/route.ts` around lines 22 - 25, The schema
currently makes widgetId and dashboardId optional allowing callers to omit them
and bypass widget write scoping; update the request validation so
widget-originated writes require explicit widget context or a server-validated
discriminator: either make widgetId and dashboardId required (remove
.optional()) for widget paths, or add a required discriminant (e.g. source:
z.enum(['widget','legacy']) or isWidget: z.boolean()) and enforce via Zod
refinements/preconditions that when source==='widget' the widgetId and
dashboardId (z.string().min(1)) must be present; then in the route handler use
that discriminant to route to the legacy branch and ensure allowWrites is
checked server-side (wherever checkAllowWrites/allowWrites logic is invoked).

Comment thread app/src/app/api/query/write/route.ts
Comment thread app/src/components/widget-editor-modal.tsx Outdated
Comment thread app/src/components/widget-editor/database-selector.tsx Outdated
Comment on lines +217 to +225
export async function listDatabases(
type: DbType,
credentials: ConnectionCredentials,
): Promise<string[]> {
const connModule = getOrCreateModule(type, credentials) as {
listDatabases: () => Promise<string[]>;
};
return connModule.listDatabases();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Credential probes can reuse a module authenticated with an old password.

These new helpers go through getOrCreateModule(), but the cache key still omits credentials.password. If the inline form changes only the password, database discovery can keep using the previously cached module and return stale success/failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/lib/query/query-executor.ts` around lines 217 - 225, The
listDatabases flow can reuse a cached module that was created with different
credentials because getOrCreateModule's cache key omits credentials.password;
update the cache-key logic used by getOrCreateModule so it includes
credentials.password (or otherwise incorporate a credentials-derived
fingerprint) so modules are keyed per exact ConnectionCredentials, and ensure
listDatabases (and other helpers like any credential probe wrappers) call
getOrCreateModule with the same credentials object; locate getOrCreateModule and
the cache/key generation logic and add the password (or secure hash of it) to
the key to prevent reusing modules authenticated with an old password.

Comment thread app/src/stores/widget-editor-store.ts Outdated
Comment on lines +43 to +55
describe("Neo4j listDatabases graceful fallback", () => {
test("should return empty array when SHOW DATABASES is not supported", async () => {
// Create a module with invalid config to simulate failure
// In practice this tests the catch path — SHOW DATABASES fails gracefully
const config = getNeo4jAuth();
const module = new Neo4jConnectionModule(config);

// The method should never throw — it returns [] on failure
const databases = await module.listDatabases();
expect(Array.isArray(databases)).toBe(true);

await module.close();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Actually force the Neo4j fallback path.

This test never makes listDatabases() fail, so it only rechecks the happy path and doesn’t verify the documented [] fallback. Mock the driver/command error and assert the empty-array contract directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@connection/__tests__/connection/list-databases.ts` around lines 43 - 55, The
test currently exercises the happy path; modify it to force the fallback by
mocking the Neo4j driver/SHOW DATABASES call to throw so listDatabases() hits
its catch path: instantiate Neo4jConnectionModule as before, stub or mock the
internal driver/session method used by Neo4jConnectionModule.listDatabases() to
throw an error (or return a rejected promise), then call module.listDatabases()
and assert it resolves to an empty array (expect(databases).toEqual([]) or
Array.isArray check), finally restore the mock and call module.close(); target
the listDatabases() method and the driver/session call it uses when setting up
the mock.

Comment thread connection/src/postgresql/PostgresConnectionModule.ts
alfredorubin96 and others added 2 commits April 30, 2026 16:05
- Add database to widget-query cache key to prevent cross-DB collisions
- Forward full config (SSL, timeouts) in list-databases-inline route
- Validate widget-connection binding and gate DB override by allowPerCardDb in write route
- Return allowPerCardDb from POST /api/connections
- Show placeholder in DatabaseSelector when no databases available
- Clear database override when switching connections in store
- Exclude information_schema and escape underscore in PG listSchemas
- Hide write mode checkbox for content-only widgets (markdown/iframe)
- Only persist database/allowWrites for non-content-only widgets
- Add 3 tests: per-card DB propagation, DB override gating, widget-connection mismatch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ides

Cover DatabaseSelector component (loading, empty, select, __default__
mapping), useConnectionDatabases hook (query key, enabled/disabled,
fetch endpoint), and per-card database override logic in the query
route (allowPerCardDb true/false/undefined, missing database field).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alfredo1996
alfredo1996 changed the base branch from dev to release/2.0 May 1, 2026 09:57
@sonarqubecloud

sonarqubecloud Bot commented May 1, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
74.3% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

…ase fields

- Add listDatabases tests: returns databases, correct credentials forwarding
- Add listSchemas tests: returns schemas, empty array when unsupported
- Add widget-editor-store tests: database/allowWrites defaults, setters,
  setConnectionId clears database, resetForAdd clears both

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/src/app/api/connections/list-databases-inline/route.ts (1)

20-30: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward the full validated config object to listing helpers.

credentials is still partial and drops supported fields (connectionAcquisitionTimeout, idleTimeout, maxRows). This can cause inline listing behavior to differ from other validated-connection flows.

♻️ Suggested fix
-    const credentials = {
-      uri: config.uri,
-      username: config.username,
-      password: config.password,
-      database: config.database,
-      sslRejectUnauthorized: config.sslRejectUnauthorized,
-      connectionTimeout: config.connectionTimeout,
-      queryTimeout: config.queryTimeout,
-      maxPoolSize: config.maxPoolSize,
-      statementTimeout: config.statementTimeout,
-    };
+    const credentials = config;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/connections/list-databases-inline/route.ts` around lines 20 -
30, The current code builds a partial `credentials` object from `config` which
omits supported fields (`connectionAcquisitionTimeout`, `idleTimeout`,
`maxRows`) and causes inline listing to diverge; update the call site that uses
`credentials` (and any listing helpers invoked from this route) to forward the
full validated `config` object instead of the trimmed `credentials`, or include
the missing fields in `credentials` so the listing helpers receive the complete
set of validated connection options (refer to the `credentials` variable and the
`config` object when making this change).
🧹 Nitpick comments (2)
app/src/app/api/query/write/__tests__/route.test.ts (1)

485-541: ⚡ Quick win

Assert the connection’s own database survives the disallowed override path.

This case decrypts credentials with no database, so it only proves "analytics" is not injected. It won’t catch a regression where the route drops an existing connection-level database/schema when allowPerCardDb is false.

Suggested assertion upgrade
     mockDecryptJson.mockReturnValue({
       uri: "bolt://localhost",
       username: "neo4j",
       password: "pass",
+      database: "primary",
     });
@@
     expect(mockExecuteQuery).toHaveBeenCalledWith(
       "neo4j",
-      { uri: "bolt://localhost", username: "neo4j", password: "pass" },
+      {
+        uri: "bolt://localhost",
+        username: "neo4j",
+        password: "pass",
+        database: "primary",
+      },
       expect.any(Object),
       { accessMode: "WRITE" },
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/query/write/__tests__/route.test.ts` around lines 485 - 541,
The test currently only verifies that the per-widget "analytics" override is not
used when allowPerCardDb is false but doesn't assert the connection-level
database is preserved; update the mocked decrypted credentials returned by
mockDecryptJson to include a connection database (e.g., database: "default_db")
and then assert mockExecuteQuery was called with those original connection
credentials/options containing that database (use mockExecuteQuery and the
POST/makeRequest flow and the widget with database "analytics" to confirm the
database in the executed call matches the connection-level value, not the widget
override).
app/src/stores/__tests__/widget-editor-store.test.ts (1)

505-539: ⚡ Quick win

Add hydration coverage for database and allowWrites.

This block only exercises setters/reset. The changed loadFromWidget() path in app/src/stores/widget-editor-store.ts Lines 377-378 is still untested, so a regression there would miss the PR’s backwards-compat behavior for legacy widgets and the persisted-path for edited widgets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/__tests__/widget-editor-store.test.ts` around lines 505 - 539,
Add unit tests that exercise the loadFromWidget hydration path for database and
allowWrites: create cases that call getState().loadFromWidget(widget) with (1) a
legacy widget missing database/allowWrites and assert state.database === "" (or
undefined per contract) and state.allowWrites === false, (2) a widget that
includes database and allowWrites and assert those values are set, and (3) an
edited/persisted widget scenario (pass the persisted-path/edited flag used by
loadFromWidget) and assert the persisted-path behavior clears or preserves
database as the implementation expects. Reference the loadFromWidget method and
getState() to locate where to add these tests and mirror the existing
setter/reset style tests for database and allowWrites.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/app/api/query/__tests__/route.test.ts`:
- Around line 395-413: The test currently only stubs mockDb.select to return []
so it doesn't verify the fast-path query included tenant scoping; update the
test ("fast-path ownership check is tenant-scoped (regression: `#572`)") to assert
the fast-path select call includes tenant filtering by inspecting the first
mockDb.select invocation (or the drizzleSelectChain input) for a tenant
predicate—e.g., replace the first mockReturnValueOnce(drizzleSelectChain([]))
with a custom stub/spy that captures the where() argument or SQL fragment and
assert it contains the tenant id (tenantId or "tenant_id") before returning [],
or add an expect on mockDb.select.mock.calls[0] to include a where
predicate/stringContaining("tenant_id") so the test fails if tenant scoping is
dropped.

In `@app/src/app/api/query/write/__tests__/route.test.ts`:
- Around line 295-408: Add a new test mirroring the existing "returns 403 when
widget allowWrites is false" case but with the widget object omitting the
allowWrites property to ensure legacy dashboards default to disallow writes; use
the same setup (mockRequireSession.mockResolvedValue(writerSession),
mockDb.select.mockReturnValueOnce(drizzleSelectChain([fakeConnection])) then
mockReturnValueOnce(drizzleSelectChain([dashboardWithWidgetMissingAllowWrites])),
call POST(makeRequest({... connectionId: "c1", query: "CREATE (n:Test)",
widgetId: "w1", dashboardId: "d1" }))), and assert res.status === 403 and
body.error.message matches the write-mode-not-enabled text; reference the
existing helpers POST, makeRequest, mockDb.select, drizzleSelectChain, and
writerSession to implement the test.

---

Duplicate comments:
In `@app/src/app/api/connections/list-databases-inline/route.ts`:
- Around line 20-30: The current code builds a partial `credentials` object from
`config` which omits supported fields (`connectionAcquisitionTimeout`,
`idleTimeout`, `maxRows`) and causes inline listing to diverge; update the call
site that uses `credentials` (and any listing helpers invoked from this route)
to forward the full validated `config` object instead of the trimmed
`credentials`, or include the missing fields in `credentials` so the listing
helpers receive the complete set of validated connection options (refer to the
`credentials` variable and the `config` object when making this change).

---

Nitpick comments:
In `@app/src/app/api/query/write/__tests__/route.test.ts`:
- Around line 485-541: The test currently only verifies that the per-widget
"analytics" override is not used when allowPerCardDb is false but doesn't assert
the connection-level database is preserved; update the mocked decrypted
credentials returned by mockDecryptJson to include a connection database (e.g.,
database: "default_db") and then assert mockExecuteQuery was called with those
original connection credentials/options containing that database (use
mockExecuteQuery and the POST/makeRequest flow and the widget with database
"analytics" to confirm the database in the executed call matches the
connection-level value, not the widget override).

In `@app/src/stores/__tests__/widget-editor-store.test.ts`:
- Around line 505-539: Add unit tests that exercise the loadFromWidget hydration
path for database and allowWrites: create cases that call
getState().loadFromWidget(widget) with (1) a legacy widget missing
database/allowWrites and assert state.database === "" (or undefined per
contract) and state.allowWrites === false, (2) a widget that includes database
and allowWrites and assert those values are set, and (3) an edited/persisted
widget scenario (pass the persisted-path/edited flag used by loadFromWidget) and
assert the persisted-path behavior clears or preserves database as the
implementation expects. Reference the loadFromWidget method and getState() to
locate where to add these tests and mirror the existing setter/reset style tests
for database and allowWrites.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6eb19092-70e7-4dea-9461-1747c17fe1d5

📥 Commits

Reviewing files that changed from the base of the PR and between 3af0545 and ca79033.

📒 Files selected for processing (14)
  • app/src/app/api/connections/list-databases-inline/route.ts
  • app/src/app/api/connections/route.ts
  • app/src/app/api/query/__tests__/route.test.ts
  • app/src/app/api/query/write/__tests__/route.test.ts
  • app/src/app/api/query/write/route.ts
  • app/src/components/widget-editor-modal.tsx
  • app/src/components/widget-editor/__tests__/database-selector.test.tsx
  • app/src/components/widget-editor/database-selector.tsx
  • app/src/hooks/__tests__/use-connection-databases.test.ts
  • app/src/hooks/use-widget-query.ts
  • app/src/lib/__tests__/query/query-executor-core.test.ts
  • app/src/stores/__tests__/widget-editor-store.test.ts
  • app/src/stores/widget-editor-store.ts
  • connection/src/postgresql/PostgresConnectionModule.ts
✅ Files skipped from review due to trivial changes (1)
  • app/src/components/widget-editor-modal.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/components/widget-editor/database-selector.tsx
  • app/src/app/api/connections/route.ts
  • app/src/app/api/query/write/route.ts

Comment on lines +395 to +413
it("fast-path ownership check is tenant-scoped (regression: #572)", async () => {
// Simulate a connection that matches userId but belongs to a different
// tenant. The fast-path WHERE clause must include tenantId so this
// connection is NOT returned.
mockRequireSession.mockResolvedValue(defaultSession);

// We need the where() call to actually filter by tenantId.
// Use a custom chain that inspects the call count to verify
// the fast-path returns empty (forcing fallback path → 404).
mockDb.select
.mockReturnValueOnce(drizzleSelectChain([])) // fast-path: no match (tenant-scoped)
.mockReturnValueOnce(drizzleJoinChain([])); // dashboard-access: no match

const res = await POST(
makeRequest({ connectionId: "c1", query: "SELECT 1" }),
);
// Connection exists for this userId but wrong tenant → 404
expect(res.status).toBe(404);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tenant isolation regression test doesn’t actually prove tenant scoping.

This test forces the fast-path query to return [], so it still passes even if the route drops tenantId from the ownership WHERE clause. Please assert the fast-path where(...) predicate includes tenant context (or equivalent SQL fragment) so the regression check is real.

Suggested tightening
-    mockDb.select
-      .mockReturnValueOnce(drizzleSelectChain([])) // fast-path: no match (tenant-scoped)
-      .mockReturnValueOnce(drizzleJoinChain([])); // dashboard-access: no match
+    const whereFastPath = vi.fn();
+    const fastPathChain = {
+      from: () => fastPathChain,
+      where: (predicate: unknown) => {
+        whereFastPath(predicate);
+        return fastPathChain;
+      },
+      limit: () => Promise.resolve([]),
+      then: (resolve: (v: unknown[]) => unknown) => Promise.resolve([]).then(resolve),
+    };
+    mockDb.select
+      .mockReturnValueOnce(fastPathChain)
+      .mockReturnValueOnce(drizzleJoinChain([]));
@@
     expect(res.status).toBe(404);
+    expect(whereFastPath).toHaveBeenCalled();
+    expect(String(whereFastPath.mock.calls[0]?.[0] ?? "")).toMatch(/tenant/i);

As per coding guidelines, "Multi-tenancy: every DB query must include tenant_id filter".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/query/__tests__/route.test.ts` around lines 395 - 413, The
test currently only stubs mockDb.select to return [] so it doesn't verify the
fast-path query included tenant scoping; update the test ("fast-path ownership
check is tenant-scoped (regression: `#572`)") to assert the fast-path select call
includes tenant filtering by inspecting the first mockDb.select invocation (or
the drizzleSelectChain input) for a tenant predicate—e.g., replace the first
mockReturnValueOnce(drizzleSelectChain([])) with a custom stub/spy that captures
the where() argument or SQL fragment and assert it contains the tenant id
(tenantId or "tenant_id") before returning [], or add an expect on
mockDb.select.mock.calls[0] to include a where
predicate/stringContaining("tenant_id") so the test fails if tenant scoping is
dropped.

Comment thread app/src/app/api/query/write/__tests__/route.test.ts
alfredorubin96 and others added 2 commits May 1, 2026 12:23
- Forward full validated config in list-databases-inline (not partial)
- Strengthen DB override test: assert connection-level database preserved
- Add legacy widget test: missing allowWrites defaults to 403
- Add loadFromWidget hydration tests for database/allowWrites fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alfredo1996 alfredo1996 closed this May 1, 2026
@alfredo1996 alfredo1996 reopened this May 1, 2026
Add listDatabases/listSchemas to module-level mock factory so
TypeScript accepts the return type when overriding in tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/src/app/api/query/write/__tests__/route.test.ts (2)

295-339: ⚡ Quick win

Assert denied paths never execute writes

These tests validate HTTP status, but for security enforcement they should also assert mockExecuteQuery is not called on 403/404 paths (Line 295, Line 410, Line 550, Line 593). That prevents regressions where a write runs before rejection.

Suggested test hardening
   expect(res.status).toBe(403);
+  expect(mockExecuteQuery).not.toHaveBeenCalled();

Apply similarly to the deny-path tests in this block.

As per coding guidelines: "ALWAYS enforce can_write permission server-side in API routes, not just in UI."

Also applies to: 410-425, 550-636

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/query/write/__tests__/route.test.ts` around lines 295 - 339,
The deny-path tests (e.g., the "returns 403 when widget allowWrites is false"
case) currently assert only HTTP status/body but do not assert that the write
worker was not invoked; update each deny-path test that sets up
mockRequireSession, mockDb.select, and calls POST(makeRequest(...)) to also
assert that mockExecuteQuery was not called (use
expect(mockExecuteQuery).not.toHaveBeenCalled() or the equivalent) after
awaiting the response; apply this to the tests around the mentioned blocks (the
403, 404 and other deny-path cases) so that functions like mockExecuteQuery,
mockRequireSession, POST, makeRequest, and any relevant widget/dashboard IDs are
referenced in the assertions to ensure no write execution occurred on rejection.

410-425: ⚡ Quick win

Add explicit widget not found test case

The route has a dedicated Widget not found in dashboard branch, but this suite currently covers dashboard not found and connection mismatch only. Add one case where dashboard exists but widgetId is absent from layoutJson.pages[].widgets, and assert 404.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/app/api/query/write/__tests__/route.test.ts` around lines 410 - 425,
Add a new test in the same suite that simulates a dashboard existing but missing
the requested widget: mock mockRequireSession to resolve writerSession, have
mockDb.select first return drizzleSelectChain([fakeConnection]) and then return
drizzleSelectChain([dashboardRecordWithLayout]) where dashboardRecordWithLayout
contains layoutJson.pages with widgets arrays that do NOT include the tested
widgetId, then call POST(makeRequest({ connectionId: "c1", query: "CREATE
(n:Test)", widgetId: "w-missing", dashboardId: "d1" })) and assert res.status is
404 to cover the "Widget not found in dashboard" branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/app/api/query/write/__tests__/route.test.ts`:
- Around line 295-339: The deny-path tests (e.g., the "returns 403 when widget
allowWrites is false" case) currently assert only HTTP status/body but do not
assert that the write worker was not invoked; update each deny-path test that
sets up mockRequireSession, mockDb.select, and calls POST(makeRequest(...)) to
also assert that mockExecuteQuery was not called (use
expect(mockExecuteQuery).not.toHaveBeenCalled() or the equivalent) after
awaiting the response; apply this to the tests around the mentioned blocks (the
403, 404 and other deny-path cases) so that functions like mockExecuteQuery,
mockRequireSession, POST, makeRequest, and any relevant widget/dashboard IDs are
referenced in the assertions to ensure no write execution occurred on rejection.
- Around line 410-425: Add a new test in the same suite that simulates a
dashboard existing but missing the requested widget: mock mockRequireSession to
resolve writerSession, have mockDb.select first return
drizzleSelectChain([fakeConnection]) and then return
drizzleSelectChain([dashboardRecordWithLayout]) where dashboardRecordWithLayout
contains layoutJson.pages with widgets arrays that do NOT include the tested
widgetId, then call POST(makeRequest({ connectionId: "c1", query: "CREATE
(n:Test)", widgetId: "w-missing", dashboardId: "d1" })) and assert res.status is
404 to cover the "Widget not found in dashboard" branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 41d5b6b7-ee9c-44b1-bc86-4c8aedb075ea

📥 Commits

Reviewing files that changed from the base of the PR and between ca79033 and fe45b90.

📒 Files selected for processing (4)
  • app/src/app/api/connections/list-databases-inline/route.ts
  • app/src/app/api/query/write/__tests__/route.test.ts
  • app/src/lib/__tests__/query/query-executor-core.test.ts
  • app/src/stores/__tests__/widget-editor-store.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/stores/tests/widget-editor-store.test.ts

@alfredo1996 alfredo1996 closed this May 1, 2026
@alfredo1996 alfredo1996 reopened this May 1, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alfredo1996
alfredo1996 force-pushed the feat/issue-568-db-selector-write-toggle branch from eeeb7ad to f7df09c Compare May 1, 2026 20:40
@alfredo1996

Copy link
Copy Markdown
Owner Author

Recreating PR with correct base branch to fix CI trigger

alfredo1996 pushed a commit that referenced this pull request May 1, 2026
- Add database to widget-query cache key to prevent cross-DB collisions
- Forward full config (SSL, timeouts) in list-databases-inline route
- Validate widget-connection binding and gate DB override by allowPerCardDb in write route
- Return allowPerCardDb from POST /api/connections
- Show placeholder in DatabaseSelector when no databases available
- Clear database override when switching connections in store
- Exclude information_schema and escape underscore in PG listSchemas
- Hide write mode checkbox for content-only widgets (markdown/iframe)
- Only persist database/allowWrites for non-content-only widgets
- Add 3 tests: per-card DB propagation, DB override gating, widget-connection mismatch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
alfredo1996 pushed a commit that referenced this pull request May 1, 2026
- Forward full validated config in list-databases-inline (not partial)
- Strengthen DB override test: assert connection-level database preserved
- Add legacy widget test: missing allowWrites defaults to 403
- Add loadFromWidget hydration tests for database/allowWrites fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(connectors): database selector + per-card write toggle

2 participants