feat(connectors): database selector + per-card write toggle (#568) - #631
feat(connectors): database selector + per-card write toggle (#568)#631alfredo1996 wants to merge 16 commits into
Conversation
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>
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughAdds 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 Changes
Sequence DiagramssequenceDiagram
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 38 minutes and 19 seconds.Comment |
There was a problem hiding this comment.
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 winThread
databaseinto the cache key.This hook now accepts a per-card database override, but
useQuerystill 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 winReturn
allowPerCardDbfrom create as well.The list endpoint now exposes the new flag, but the POST response still returns a partial row. That leaves
useCreateConnection()withallowPerCardDb === undefinedfor 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 winCover 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
⛔ Files ignored due to path filters (9)
app/drizzle/migrations/0003_furry_scourge.sqlis excluded by!app/drizzle/migrations/**app/drizzle/migrations/meta/0003_snapshot.jsonis excluded by!app/drizzle/migrations/**app/drizzle/migrations/meta/_journal.jsonis excluded by!app/drizzle/migrations/**screenshots/01-login.pngis excluded by!**/*.pngscreenshots/02-dashboards-home.pngis excluded by!**/*.pngscreenshots/03-dashboard-edit.pngis excluded by!**/*.pngscreenshots/04-widget-editor-data-tab.pngis excluded by!**/*.pngscreenshots/05-advanced-tab-write-mode-off.pngis excluded by!**/*.pngscreenshots/06-advanced-tab-write-mode-on.pngis excluded by!**/*.png
📒 Files selected for processing (23)
app/src/app/(dashboard)/[id]/edit/page.tsxapp/src/app/api/connections/[id]/databases/__tests__/route.test.tsapp/src/app/api/connections/[id]/databases/route.tsapp/src/app/api/connections/list-databases-inline/__tests__/route.test.tsapp/src/app/api/connections/list-databases-inline/route.tsapp/src/app/api/connections/route.tsapp/src/app/api/query/route.tsapp/src/app/api/query/write/__tests__/route.test.tsapp/src/app/api/query/write/route.tsapp/src/components/card-container.tsxapp/src/components/widget-editor-modal.tsxapp/src/components/widget-editor/database-selector.tsxapp/src/hooks/use-connection-databases.tsapp/src/hooks/use-connections.tsapp/src/hooks/use-query-execution.tsapp/src/hooks/use-widget-query.tsapp/src/lib/db/schema.tsapp/src/lib/query/query-executor.tsapp/src/stores/widget-editor-store.tsconnection/__tests__/connection/list-databases.tsconnection/src/generalized/ConnectionModule.tsconnection/src/neo4j/Neo4jConnectionModule.tsconnection/src/postgresql/PostgresConnectionModule.ts
| /** Per-card database override — used when the connection allows per-card DB selection. */ | ||
| database: z.string().optional(), |
There was a problem hiding this comment.
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.
| /** 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(), |
There was a problem hiding this comment.
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).
| export async function listDatabases( | ||
| type: DbType, | ||
| credentials: ConnectionCredentials, | ||
| ): Promise<string[]> { | ||
| const connModule = getOrCreateModule(type, credentials) as { | ||
| listDatabases: () => Promise<string[]>; | ||
| }; | ||
| return connModule.listDatabases(); | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
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.
- 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>
|
…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>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
app/src/app/api/connections/list-databases-inline/route.ts (1)
20-30:⚠️ Potential issue | 🟠 Major | ⚡ Quick winForward the full validated config object to listing helpers.
credentialsis 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 winAssert 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 whenallowPerCardDbisfalse.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 winAdd hydration coverage for
databaseandallowWrites.This block only exercises setters/reset. The changed
loadFromWidget()path inapp/src/stores/widget-editor-store.tsLines 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
📒 Files selected for processing (14)
app/src/app/api/connections/list-databases-inline/route.tsapp/src/app/api/connections/route.tsapp/src/app/api/query/__tests__/route.test.tsapp/src/app/api/query/write/__tests__/route.test.tsapp/src/app/api/query/write/route.tsapp/src/components/widget-editor-modal.tsxapp/src/components/widget-editor/__tests__/database-selector.test.tsxapp/src/components/widget-editor/database-selector.tsxapp/src/hooks/__tests__/use-connection-databases.test.tsapp/src/hooks/use-widget-query.tsapp/src/lib/__tests__/query/query-executor-core.test.tsapp/src/stores/__tests__/widget-editor-store.test.tsapp/src/stores/widget-editor-store.tsconnection/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
| 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); | ||
| }); |
There was a problem hiding this comment.
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.
- 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>
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>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
app/src/app/api/query/write/__tests__/route.test.ts (2)
295-339: ⚡ Quick winAssert denied paths never execute writes
These tests validate HTTP status, but for security enforcement they should also assert
mockExecuteQueryis not called on403/404paths (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_writepermission 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 winAdd explicit
widget not foundtest caseThe route has a dedicated
Widget not found in dashboardbranch, but this suite currently coversdashboard not foundand connection mismatch only. Add one case where dashboard exists butwidgetIdis absent fromlayoutJson.pages[].widgets, and assert404.🤖 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
📒 Files selected for processing (4)
app/src/app/api/connections/list-databases-inline/route.tsapp/src/app/api/query/write/__tests__/route.test.tsapp/src/lib/__tests__/query/query-executor-core.test.tsapp/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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
eeeb7ad to
f7df09c
Compare
|
Recreating PR with correct base branch to fix CI trigger |
- 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>
- 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>


Summary
listDatabases()for Neo4j (SHOW DATABASES, graceful fallback for < 4.x) and PostgreSQL (pg_database + information_schema.schemata for schemas)allow_per_card_db, default true)canWrite=true. Server enforces by looking up the widget in the dashboard layoutGET /connections/[id]/databasesandPOST /connections/list-databases-inlinefor fetching available databasesallow_per_card_dbcolumn to connections tableScreenshots
Test plan
listDatabasesfor Neo4j + PostgreSQL via Testcontainers (10 tests)Closes #568
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests