Skip to content

feat: allow field/arg toggling when selection set is empty#39

Open
david-on-github wants to merge 4 commits into
mainfrom
david/feat/query-empty-selection
Open

feat: allow field/arg toggling when selection set is empty#39
david-on-github wants to merge 4 commits into
mainfrom
david/feat/query-empty-selection

Conversation

@david-on-github

@david-on-github david-on-github commented Jul 5, 2026

Copy link
Copy Markdown
Member

Fixes #24

Summary

  • GraphQL's `parse()` rejects empty selection sets (`{ }`), causing all sidebar field and arg buttons to be disabled whenever the query contained one
  • `patchEmptySelectionSets()` repairs unparseable queries by replacing empty `{ }` with a `{ __typename }` placeholder, using a negative lookbehind so ObjectValue args like `filter: {}` are left alone
  • All toggle functions fall back to patch-then-restore when `parse()` throws, then strip the placeholder before returning the result
  • Fixed a silent no-op in `ensureArgAndToggleInputField` where toggling into a nested empty ObjectValue (e.g. `filter: { someField: {} }`) returned the query unchanged — `hasInputObjectInQuery` found the type via its own fallback so no exception was raised, bypassing the retry path
  • `getCursorContext` falls back to a text scan so the sidebar auto-navigates when the cursor is inside an empty selection set or an empty ObjectValue arg
  • Field insertion now derives indentation from the close-brace line, avoiding stray blank lines when inserting into an empty selection set
  • Changed `DEFAULT_QUERY` to an empty string so the editor starts blank rather than with a placeholder
  • Removed the `disabled={!inQuery}` guard from the Fields section in `SchemaExplorer` — buttons should be clickable before a selection set exists

Test plan

  • Open the Query Runner with an empty query — click any collection in the sidebar, fields and args should be clickable and insert correctly
  • Write a query with an empty selection set and place the cursor inside it — sidebar should auto-navigate to the correct type
  • Click a field while the selection set is empty — it should insert without blank lines above or below it
  • Add a filter arg with a nested empty ObjectValue (e.g. `filter: { someField: {} }`) and click into the inner `{}` — sidebar should navigate to the correct operator block
  • Click an operator from that block — it should insert into the nested `{}` with no `__typename` left in the query
  • Confirm toggling fields works normally on a valid, non-empty query (no regressions)

Fixes #24. GraphQL's parser rejects empty selection sets ({ }), which
previously caused all sidebar buttons to go disabled when the query
editor contained one.

- Changed DEFAULT_QUERY to empty string so the editor starts parseable
- Added patchEmptySelectionSets() helper (negative lookbehind to avoid
  touching ObjectValue args like filter: {})
- Added findRootFieldSelectionSet() text scanner to locate a root
  field's selection set without parsing
- toggleFieldInQuery, toggleArgInQuery, canToggleInputType,
  ensureArgAndToggleInputField all fall back to patch-then-restore when
  parse() throws on an empty selection set
- Field insertion into empty selection sets derives indentation from the
  close-brace's actual line position, avoiding stray blank lines
- getCursorContext falls back to a text scan for root operation
  detection when the query is unparseable, so the sidebar auto-navigates
  correctly when the cursor is inside an empty selection set
- Removed disabled={!inQuery} from the Fields section buttons in
  SchemaExplorer so fields can be clicked before a selection set exists
- Fixed React duplicate key warning in DashboardView peers list
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@david-on-github, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c373b7d-871b-440a-8eda-3afc0a5bc1dc

📥 Commits

Reviewing files that changed from the base of the PR and between ba55e8b and 4cd2778.

📒 Files selected for processing (2)
  • src/lib/queryBuilder.test.ts
  • src/lib/queryBuilder.ts
📝 Walkthrough

Walkthrough

Query builder logic gains parse-failure recovery for empty GraphQL selection sets ({}), using text-patching helpers to retry toggle/detection operations. UI controls that previously disabled interactions when a root field was absent from the query are un-gated, the default query becomes an empty string, tests are added, and an unrelated peers-list key change is included.

Changes

Empty Selection Set Query Toggling

Layer / File(s) Summary
Patch/scan helpers
src/lib/queryBuilder.ts
Adds patchEmptySelectionSets() and findRootFieldSelectionSet() for text-based detection and repair of empty {} selection sets.
toggleArgInQuery recovery
src/lib/queryBuilder.ts
On parse failure, patches the root field's empty selection set, retries the toggle, and restores original text.
toggleFieldInQuery insertion/removal recovery
src/lib/queryBuilder.ts
Filters stale __typename placeholders before insertion and adds a removal-path recovery for empty selection sets with derived indentation.
Input object toggling retries
src/lib/queryBuilder.ts
getInputObjectFieldsAtOffset, toggleInputObjectFieldAtOffset, hasInputObjectInQuery, isRootFieldInQuery, canToggleInputType, and ensureArgAndToggleInputField retry with patched empty selection sets on parse failure.
getCursorContext fallback scanning
src/lib/queryBuilder.ts
Adds schema-informed text scanning fallback to detect cursor position inside input-object or field selection-set braces.
UI gating removal and empty default query
src/components/SchemaExplorer.tsx, src/views/QueryView.tsx
Removes disabled={!inQuery} from select-all and per-field controls; DEFAULT_QUERY changes from a braced scaffold to an empty string.
Empty selection set fallback tests
src/lib/queryBuilder.test.ts
Adds canToggleInputType import and a new "Empty selection set fallbacks" test suite for issue #24.

Peers List Rendering

Layer / File(s) Summary
Peers list key change
src/views/DashboardView.tsx
Connected Peers map callback now uses (p, i) and keys rows by array index instead of peer id.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SchemaExplorer
  participant QueryBuilder
  participant GraphQLParser

  SchemaExplorer->>QueryBuilder: toggleFieldInQuery(query, field)
  QueryBuilder->>GraphQLParser: parse(query)
  GraphQLParser-->>QueryBuilder: parse error (empty selection set)
  QueryBuilder->>QueryBuilder: patchEmptySelectionSets(query)
  QueryBuilder->>GraphQLParser: parse(patched query)
  GraphQLParser-->>QueryBuilder: AST
  QueryBuilder->>QueryBuilder: apply toggle, restore original braces
  QueryBuilder-->>SchemaExplorer: updated query
Loading

Assessment against linked issues

Objective Addressed Explanation
Allow toggling fields into an empty selection set query (#24)
Remove UI disabling of sidebar selections when query is invalid/empty (#24)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Peers list map callback changed to use index as React key (src/views/DashboardView.tsx) Unrelated to issue #24's query-toggling scope; concerns Connected Peers rendering keys.

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

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 5, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
defradb-explorer 4cd2778 Commit Preview URL

Branch Preview URL
Jul 05 2026, 05:21 PM

Covers the catch-block repair logic in toggleFieldInQuery,
toggleArgInQuery, isRootFieldInQuery, canToggleInputType,
ensureArgAndToggleInputField, and getCursorContext for queries
containing empty selection sets that parse() rejects.

Also removes the dead '!ssInfo' fallback path from toggleFieldInQuery
that would incorrectly build a fresh document for garbled input.
Extends the empty-selection-set fallback to cover the case where the
cursor is inside an ObjectValue arg (e.g. filter: {}) while the query
also has an empty selection set.

- getCursorContext catch block now text-scans each root field's arg list
  for input-object args and returns insertObject when the cursor is
  found inside one, enabling sidebar auto-navigation to the filter type
- toggleInputObjectFieldAtOffset gains a patchEmptySelectionSets
  fallback so clicking filter fields actually modifies the query
- getInputObjectFieldsAtOffset gains the same fallback so toggled
  fields show as selected after insertion
@david-on-github david-on-github self-assigned this Jul 5, 2026
@david-on-github david-on-github marked this pull request as ready for review July 5, 2026 16:54
@david-on-github david-on-github requested a review from nasdf July 5, 2026 16:55

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/queryBuilder.ts`:
- Around line 244-262: findRootFieldSelectionSet() currently matches the first
textual rootFieldName anywhere in the query, which can target nested or
unrelated occurrences; update the fallback scan to track brace/operation depth
and only accept rootFieldName when it appears in the top-level selection set.
Keep the existing selection parsing logic in src/lib/queryBuilder.ts, but gate
the RegExp match and subsequent scan on root-level context so fallback toggles
patch the correct selection set.
- Line 235: The patch in patchEmptySelectionSets() is too broad because it
rewrites any empty braces that are not after : or [, so it still mutates empty
object values after commas. Update queryBuilder.ts to use a more context-aware
scan (or extend the matcher) so only true empty selection sets are patched, and
exclude object/list value positions such as commas and default-value contexts;
keep the fix localized to patchEmptySelectionSets() and its return query.replace
logic.
- Line 235: The fallback-path changes in queryBuilder are tripping ESLint: clean
up the regex in the query replacement logic by removing the unnecessary escaping
in the character class, and update the empty catch blocks in the related
fallback sections to include explicit comments so they are not flagged. Apply
the same lint-safe cleanup in the affected fallback helpers around the
queryBuilder flow, including the logic near the query.replace call and the empty
catches referenced in the review.
- Around line 1335-1342: The argument lookup in queryBuilder’s field parsing is
scanning beyond the current field’s parentheses, so later arguments can be
incorrectly associated with the current field. In the logic around the
`query[pos] === '('` block, first find the matching closing parenthesis for the
current field, then restrict the `argMatch` search to that slice instead of
`query.slice(argListStart)`. Keep the fix local to this parser path so
`fieldDef.args`, `getNamedType`, and `isInputObjectType` still behave the same.
- Line 159: The placeholder restoration in queryBuilder is using a first-match
replace, which can target the wrong __typename occurrence or miss additional
patched spots. Update the logic around the toggled query reconstruction to
restore only the specific empty selection set that was originally patched,
preferably by tracking the patched range(s) or re-locating the exact selection
set before replacement. Apply the same fix to the other __typename restore sites
in queryBuilder so each patched placeholder is matched deterministically rather
than by the first string occurrence.

In `@src/views/DashboardView.tsx`:
- Around line 38-39: The peers list in DashboardView should use a stable unique
key instead of the array index. Update the map in DashboardView.tsx to key each
row by p.id, matching the existing PeersView.tsx pattern, and keep the row
rendering tied to the peer identity rather than position. If duplicate key
warnings were the original concern, fix the source of non-unique peer ids
instead of switching to index-based keys.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96e545cf-0aaa-41a4-9cb8-25a6cc14b746

📥 Commits

Reviewing files that changed from the base of the PR and between f11f8e7 and ba55e8b.

📒 Files selected for processing (5)
  • src/components/SchemaExplorer.tsx
  • src/lib/queryBuilder.test.ts
  • src/lib/queryBuilder.ts
  • src/views/DashboardView.tsx
  • src/views/QueryView.tsx
💤 Files with no reviewable changes (1)
  • src/components/SchemaExplorer.tsx
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Workers Builds: defradb-explorer
🧰 Additional context used
🪛 ast-grep (0.44.1)
src/lib/queryBuilder.ts

[warning] 243-243: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\b${rootFieldName}\\b)
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)


[warning] 1330-1330: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\b${fieldName}\\b)
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)


[warning] 1339-1339: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\b${arg.name}\\s*:\\s*\\{)
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)


[warning] 1361-1361: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\b${fieldName}\\s*(?:\\([^)]*\\))?\\s*\\{, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🪛 ESLint
src/lib/queryBuilder.ts

[error] 235-235: Unnecessary escape character: [.

(no-useless-escape)


[error] 784-784: Unnecessary escape character: [.

(no-useless-escape)


[error] 788-788: Empty block statement.

(no-empty)


[error] 966-966: Empty block statement.

(no-empty)


[error] 1012-1012: Unnecessary escape character: [.

(no-useless-escape)


[error] 1013-1013: Empty block statement.

(no-empty)

🪛 OpenGrep (1.23.0)
src/lib/queryBuilder.ts

[ERROR] 244-244: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 1331-1331: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 1340-1340: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 1364-1364: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (3)
src/views/QueryView.tsx (1)

49-49: LGTM!

src/lib/queryBuilder.test.ts (1)

20-21: LGTM!

Also applies to: 969-1073

src/lib/queryBuilder.ts (1)

423-431: LGTM!

Comment thread src/lib/queryBuilder.ts
Comment thread src/lib/queryBuilder.ts Outdated
Comment thread src/lib/queryBuilder.ts
Comment on lines +244 to +262
const m = new RegExp(`\\b${rootFieldName}\\b`).exec(query)
if (!m) return null
let i = m.index + m[0].length
while (i < query.length && /[ \t]/.test(query[i])) i++
if (query[i] === '(') {
let d = 1; i++
while (i < query.length && d > 0) {
if (query[i] === '(') d++; else if (query[i] === ')') d--
i++
}
}
while (i < query.length && /\s/.test(query[i])) i++
if (i >= query.length || query[i] !== '{') return null
const start = i; let d = 1; i++
while (i < query.length && d > 0) {
if (query[i] === '{') d++; else if (query[i] === '}') d--
i++
}
return { start, end: i, text: query.slice(start, i) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Constrain the fallback scan to root-level fields.

findRootFieldSelectionSet() returns the first textual rootFieldName anywhere in the query. A nested Post { } or a same-name token before the actual root field can make fallback toggles patch the wrong selection set or fail to recover. Track operation/brace depth and only accept matches in the operation’s top-level selection set.

🧰 Tools
🪛 OpenGrep (1.23.0)

[ERROR] 244-244: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/queryBuilder.ts` around lines 244 - 262, findRootFieldSelectionSet()
currently matches the first textual rootFieldName anywhere in the query, which
can target nested or unrelated occurrences; update the fallback scan to track
brace/operation depth and only accept rootFieldName when it appears in the
top-level selection set. Keep the existing selection parsing logic in
src/lib/queryBuilder.ts, but gate the RegExp match and subsequent scan on
root-level context so fallback toggles patch the correct selection set.

Comment thread src/lib/queryBuilder.ts
Comment on lines +38 to +39
{peers!.map((p, i) => (
<div key={i} className={styles.peerRow}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Revert to keying by p.id instead of array index.

Using the index as a React key for a dynamic, externally-fetched peers list is an anti-pattern — it breaks reconciliation when peers connect/disconnect or reorder, causing stale DOM/state per row. p.id is already used as a stable, unique key in PeersView.tsx for the same peer shape, so this should be consistent here rather than regressing to index-based keys.

🔧 Proposed fix
-                {peers!.map((p, i) => (
-                  <div key={i} className={styles.peerRow}>
+                {peers!.map(p => (
+                  <div key={p.id} className={styles.peerRow}>

If the intent was to fix a duplicate-key warning caused by non-unique p.id values, the actual fix should address the source of duplicate ids rather than discarding stable keys.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{peers!.map((p, i) => (
<div key={i} className={styles.peerRow}>
{peers!.map(p => (
<div key={p.id} className={styles.peerRow}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/views/DashboardView.tsx` around lines 38 - 39, The peers list in
DashboardView should use a stable unique key instead of the array index. Update
the map in DashboardView.tsx to key each row by p.id, matching the existing
PeersView.tsx pattern, and keep the row rendering tied to the peer identity
rather than position. If duplicate key warnings were the original concern, fix
the source of non-unique peer ids instead of switching to index-based keys.

ensureArgAndToggleInputField was silently no-oping when the query had an
empty selection set and the target ObjectValue was nested (e.g. filter: {
title: {} }). hasInputObjectInQuery used its own patchEmptySelectionSets
fallback to find the type without throwing, then toggleInputObjectField
caught the parse error and returned the query unchanged — no exception was
raised, so the outer try/catch never fell through to the patch+retry path.

Fix: compare the return value of run(query) against the original query and
only return early if something actually changed; otherwise fall through to
the patched retry path.

Adds tests for both toggleInputObjectFieldAtOffset and
ensureArgAndToggleInputField with a nested empty ObjectValue and empty SS.

Co-Authored-By: Claude Sonnet 4.6 <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.

Query Runner can't toggle fields on an empty selection set

1 participant