Skip to content

feat: Add contains-substring filter for String fields#3392

Open
dblythy wants to merge 1 commit into
parse-community:alphafrom
dblythy:fix/2153-contains-substring-filter
Open

feat: Add contains-substring filter for String fields#3392
dblythy wants to merge 1 commit into
parse-community:alphafrom
dblythy:fix/2153-contains-substring-filter

Conversation

@dblythy

@dblythy dblythy commented Jul 5, 2026

Copy link
Copy Markdown
Member

Closes #2153

Problem

For a String column, the only substring-style filter is matches regex, which passes the input to Parse.Query#matches verbatim as a regular expression. Regex metacharacters therefore have special meaning: . matches any character, and metacharacters like ( are invalid on their own, so a plain literal search either returns the wrong rows or fails with a server error. There is no way to do a simple literal substring search.

Fix

Add a separate contains substring operator for String fields, backed by the SDK's Parse.Query#contains, which wraps the input in \Q...\E so the whole value is matched literally (the same quoting startsWith / endsWith already use). The existing matches regex operator is unchanged, so raw regex remains available.

The "rename the confusing operator" half of the issue was already handled in #2991 (the old mislabelled entry was replaced by the explicit matches regex), so this PR only adds the new safe operator. This does not touch the Array-field labels discussed in #2052.

Unit test added in src/lib/tests/queryFromFilters.test.js covering literal substring matching, metacharacter escaping, and a contrast asserting matches stays a raw regex.

The new operator

The String field condition list now offers contains substring below matches regex:

new option

Literal . (data: a(b, c.d, hello, world)

matches regex treats . as a wildcard and returns every row:

matches regex dot

contains substring matches . literally and returns only c.d:

contains dot

Literal a(b

matches regex with a(b is an invalid regular expression, so the query fails (500) and nothing is shown:

matches regex paren

contains substring with a(b matches the parenthesis literally and returns the correct row:

contains paren

Summary by CodeRabbit

  • New Features

    • Added support for a new string filter option, allowing substring matching in queries.
    • Expanded the available string constraint list to include the new option.
  • Bug Fixes

    • Query filtering now correctly applies substring matching when this new constraint is used.
    • Added test coverage to ensure substring and regex-based filters behave as expected, including literal handling of special characters.

@parse-github-assistant

Copy link
Copy Markdown

🚀 Thanks for opening this pull request! We appreciate your effort in improving the project. Please let us know once your pull request is ready for review.

Tip

  • Keep pull requests small. Large PRs will be rejected. Break complex features into smaller, incremental PRs.
  • Use Test Driven Development. Write failing tests before implementing functionality. Ensure tests pass.
  • Group code into logical blocks. Add a short comment before each block to explain its purpose.
  • We offer conceptual guidance. Coding is up to you. PRs must be merge-ready for human review.
  • Our review focuses on concept, not quality. PRs with code issues will be rejected. Use an AI agent.
  • Human review time is precious. Avoid review ping-pong. Inspect and test your AI-generated code.

Note

Please respond to review comments from AI agents just like you would to comments from a human reviewer. Let the reviewer resolve their own comments, unless they have reviewed and accepted your commit, or agreed with your explanation for why the feedback was incorrect.

Caution

Pull requests must be written using an AI agent with human supervision. Pull requests written entirely by a human will likely be rejected, because of lower code quality, higher review effort and the higher risk of introducing bugs. Please note that AI review comments on this pull request alone do not satisfy this requirement. Our CI and AI review are safeguards, not development tools. If many issues are flagged, rethink your development approach. Invest more effort in planning and design rather than using review cycles to fix low-quality code.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10a2e002-e944-4f20-871e-dd74472116e9

📥 Commits

Reviewing files that changed from the base of the PR and between 9db24be and cbbb047.

📒 Files selected for processing (3)
  • src/lib/Filters.js
  • src/lib/queryFromFilters.js
  • src/lib/tests/queryFromFilters.test.js

📝 Walkthrough

Walkthrough

Adds a new contains string constraint to the filter constraint catalog and allowed constraints list, adds a corresponding handler in addConstraintFromValues that calls query.contains, and adds tests validating substring escaping and existing matches regex behavior.

Changes

Contains Substring Constraint

Layer / File(s) Summary
Constraint definition and field list
src/lib/Filters.js
Adds a contains entry to the Constraints catalog (substring check, composable, comparable) and includes 'contains' in the FieldConstraints.String allowed list.
Query builder wiring and tests
src/lib/queryFromFilters.js, src/lib/tests/queryFromFilters.test.js
Adds a case 'contains' in addConstraintFromValues that calls query.contains(field, String(compareTo)), and adds a new test module verifying escaped $regex for contains and raw regex/$options behavior for matches.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User as Dashboard User
  participant Filter as Filters UI
  participant Builder as addConstraintFromValues
  participant Query as Parse.Query

  User->>Filter: select "contains substring" constraint
  Filter->>Builder: addConstraintFromValues(field, 'contains', value)
  Builder->>Query: query.contains(field, String(value))
  Query-->>Filter: returns matching records via escaped regex
Loading

Related Issues: #2153

Suggested labels: enhancement, filters

Suggested reviewers: dblythy, mtrezza

🐰 A whisker twitches, sniffing through strings so tight,
No more escaping stray parentheses at night,
"Contains" now means what it plainly should say,
While "matches" keeps its regex power at play,
A tiny carrot of code, tucked in with delight.

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Engage In Review Feedback ❓ Inconclusive Repo only shows the final feature commit; no review discussion/comment history is present, so feedback engagement can't be verified. Provide PR review thread or evidence of discussion plus follow-up commit/retraction; otherwise this check remains unverifiable.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required feat: prefix and clearly matches the new String contains-substring filter.
Description check ✅ Passed The description covers the issue, approach, and testing, though it does not follow the template headings exactly.
Linked Issues check ✅ Passed The PR implements #2153 by adding literal contains matching while preserving regex-based matches behavior and adding tests.
Out of Scope Changes check ✅ Passed The changes are limited to the new String contains operator, query handling, and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Security Check ✅ Passed The change only adds a literal substring filter via Parse.Query.contains; the regex path is unchanged and no new unsafe sink is introduced.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@mtrezza

mtrezza commented Jul 5, 2026

Copy link
Copy Markdown
Member

I don't understand what this PR is trying to achieve? What's the purpose of adding a "contains" filter, which was removed in #2991 because it was just a subset of "matches regex"?

@dblythy

dblythy commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

Oh, I didn't realise this previously existed. I thought the ticket was about adding the .contains method that Parse Server support (even in the PR you linked it was a regexp)

FWIW startsWith, endsWith can also just be done with a regexp.

Happy to close though if you prefer

@mtrezza

mtrezza commented Jul 9, 2026

Copy link
Copy Markdown
Member

@coderabbitai how are startsWith, endsWith implemented? Do they have the same issue as #2991 with a fixed "i" regex option?

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---Filters.js startsWith/endsWith---"
rg -n -B2 -A5 "startsWith|endsWith" src/lib/Filters.js
echo "---queryFromFilters.js startsWith/endsWith---"
rg -n -B2 -A10 "startsWith|endsWith" src/lib/queryFromFilters.js
echo "---full switch block---"
rg -n "case '" src/lib/queryFromFilters.js

Length of output: 665


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---Filters.js full---"
cat -n src/lib/Filters.js
echo "---queryFromFilters.js full---"
cat -n src/lib/queryFromFilters.js

Length of output: 20102


🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -type f "ParseQuery.js" 2>/dev/null || true
fd . node_modules/parse/lib -e js | rg -i "ParseQuery" || true
echo "---search startsWith/endsWith/contains/quote in parse package---"
rg -n -A15 "startsWith\s*\(" node_modules/parse/lib/node/ParseQuery.js 2>/dev/null || rg -n -A15 "startsWith\s*\(" $(fd -a ParseQuery.js node_modules 2>/dev/null | head -1)
echo "---"
rg -n -A15 "endsWith\s*\(" $(fd -a ParseQuery.js node_modules 2>/dev/null | head -1)
echo "---contains---"
rg -n -A15 "\bcontains\s*\(" $(fd -a ParseQuery.js node_modules 2>/dev/null | head -1)
echo "---quote---"
rg -n -A10 "quote\s*\(" $(fd -a ParseQuery.js node_modules 2>/dev/null | head -1)

Length of output: 1981


🏁 Script executed:

#!/bin/bash
set -euo pipefail
F=node_modules/parse/lib/node/ParseQuery.js
echo "---_regexStartWith---"
rg -n -A10 "_regexStartWith" "$F"
echo "---endsWith---"
rg -n -A15 "^\s*endsWith\s*\(" "$F"
echo "---contains(---"
rg -n -A15 "^\s*contains\s*\(" "$F"
echo "---matches(---"
rg -n -A20 "^\s*matches\s*\(" "$F"
echo "---quote/escape---"
rg -n -A15 "quote\(|_quote\(" "$F"
echo "---function quote definition---"
rg -n -B2 -A15 "function quote" "$F"

Length of output: 6990


@mtrezza Here's how startsWith/endsWith are implemented in this codebase and whether they share the "fixed i option" issue from #2991:

In src/lib/queryFromFilters.js:

  • starts (Line 206): query.startsWith(field, compareTo) (or, for a Pointer field, pointerQuery.startsWith('objectId', compareTo.objectId) then matchesQuery).
  • ends (Line 210): query.endsWith(field, compareTo).

Neither call passes a modifiers argument, so no $options (e.g., 'i') is set by the dashboard at all — these delegate entirely to the Parse JS SDK's own startsWith/endsWith methods with no case-insensitivity flag forced.

Inside the Parse JS SDK (node_modules/parse/lib/node/ParseQuery.js):

  • startsWith(key, prefix, modifiers)this.matches(key, this._regexStartWith(prefix), modifiers), where _regexStartWith(string) returns '^' + quote(string).
  • endsWith(key, suffix, modifiers)this.matches(key, quote(suffix) + '$', modifiers).
  • contains(key, substring)this._addCondition(key, '$regex', quote(substring)) — note it doesn't even accept a modifiers param.
  • matches(key, regex, modifiers) only adds $options if modifiers is truthy (or if a native RegExp object with .ignoreCase/.multiline is passed) — there's no hardcoded 'i'.
  • quote(s) wraps the substring in MongoDB's \Q...\E literal-quoting syntax ('\\Q' + s.replace('\\E', '\\E\\\\E\\Q') + '\\E'), which is functionally similar to Java's Pattern.quote — it neutralizes regex metacharacters within the quoted string.

Conclusion: startsWith and endsWith do not have the same issue as the old #2991 "contains" implementation. That older dashboard-side implementation apparently hard-coded a case-insensitive 'i' $options modifier alongside treating raw user input as regex (unescaped), which is why it was removed as "just a subset of matches regex" with unwanted side effects. The current startsWith/endsWith/contains all rely on the Parse JS SDK's own quote() literal-escaping and don't force any $options unless explicitly requested — so they're safe from that specific bug, and functionally consistent with what this PR's new contains (substring) filter does (it also just calls query.contains(field, String(compareTo))).

@mtrezza

mtrezza commented Jul 9, 2026

Copy link
Copy Markdown
Member

@coderabbitai The issue in #2991 was also that no options can be customized, so emojis (/u option) for example are not recognized.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---modifiers refs---"
rg -n "modifiers" src --type=js -C3
echo "---gear icon / settings component in filter row---"
rg -n "gear|Gear" src/components -i -C2 | head -80

Length of output: 12098


@mtrezza You're right to flag this, and here's the precise picture based on the code:

matches regex (unchanged by this PR): already supports customizable modifiers via the gear icon (RegexOptionsButton in src/components/BrowserFilter/FilterRow.react.js), which lets a user toggle i, u, m, x, s flags. These are passed through as the modifiers argument to query.matches(field, String(compareTo), modifiers) in src/lib/queryFromFilters.js (Line 233), and forwarded as $options on the $regex condition. So /u for emoji/Unicode-aware matching is already available today for matches regex — that part of the old #2991 problem (hardcoded, non-customizable options) doesn't apply here since it's a separate, existing operator.

contains substring (new in this PR): calls query.contains(field, String(compareTo)) (Line 235-236) with no modifiers at all. This isn't an oversight — the Parse JS SDK's contains() method itself doesn't accept a modifiers parameter; it just wraps the substring in quote() and adds it as a plain $regex with no $options. Consistent with that, src/components/Filter/Filter.react.js (Line 80) only defaults modifiers to 'i' when the constraint is matches, and the gear icon (RegexOptionsButton) is likewise not rendered for contains.

So the practical consequence: contains substring is always case-sensitive and has no way to enable Unicode-aware (/u) or any other flag — it's literal-only, full stop. If a user needs case-insensitive or Unicode-aware substring matching, they'd have to fall back to matches regex with the metacharacters manually escaped (or a hand-built pattern), rather than being able to just check an "i" or "u" box on contains substring.

If you'd like, I can treat this as a follow-up scope item — e.g., either (a) exposing an $options-equivalent for contains by falling back to a manually-quoted $regex with $options instead of using the SDK's contains() shortcut, or (b) documenting this limitation clearly in the UI. Let me know which direction you'd prefer, or if you'd rather fold this into the current PR before merging.

@mtrezza

mtrezza commented Jul 10, 2026

Copy link
Copy Markdown
Member

@coderabbitai We need a unified solution; the same issue exists with startsWith, endsWith, no matching flexibility, which is exactly why #2991 was merged. These kind of filter conditions with hard-coded, arbitrary, unintuitive matching limitations are impractical. How can we allow the user to quickly access a startsWith/endsWith/contains functionality and optionally use unescaped strings, given the already implemented features?

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---Filter.react.js modifiers/gear---"
rg -n -B3 -A10 "modifiers|RegexOptionsButton" src/components/Filter/Filter.react.js
echo "---FilterRow.react.js gear icon render condition---"
rg -n -B3 -A15 "RegexOptionsButton" src/components/BrowserFilter/FilterRow.react.js

Length of output: 3836


@mtrezza You're right that a unified solution is the correct path here, and I don't think this needs to expand scope dramatically — it's a coordinated change across three files, reusing what already exists for matches regex. Here's my concrete proposal:

The core idea: Keep the existing safety property (starts/ends/contains are literal-by-default, no manual escaping needed) but let all four "regex-family" operators (starts, ends, contains, matches) share the same modifiers UI and pass those flags through to the underlying $regex/$options. The only reason contains substring currently can't have modifiers is that it's calling the SDK's query.contains() shortcut, which structurally doesn't accept a modifiers argument at all — so we stop using that shortcut.

1. src/lib/queryFromFilters.js — build the regex manually instead of delegating to contains():

// Instead of:
case 'contains':
  query.contains(field, String(compareTo));
  break;

// Do (mirrors what SDK's contains()/quote() does, but keeps modifiers):
case 'contains': {
  const escaped = String(compareTo).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  query.matches(field, escaped, modifiers);
  break;
}

For starts/ends, no query-builder change is strictly needed — query.startsWith(field, compareTo, modifiers) / query.endsWith(field, compareTo, modifiers) already accept and forward modifiers to the SDK's own matches() call; the dashboard just currently never passes that third argument. So:

case 'starts':
  query.startsWith(field, compareTo, modifiers);
  break;
case 'ends':
  query.endsWith(field, compareTo, modifiers);
  break;

2. src/components/Filter/Filter.react.js — stop gating the default modifier to matches only:

// Line 80, currently:
modifiers: newConstraint === 'matches' ? 'i' : undefined,

// Change to cover the whole regex family:
modifiers: ['matches', 'starts', 'ends', 'contains'].includes(newConstraint) ? 'i' : undefined,

(Open question: should the default still auto-check i, or should it start unchecked for the new literal operators so behavior doesn't silently change for existing starts/ends users? I'd lean toward leaving new/changed operators unchecked by default and letting the user opt in, to avoid a surprise case-insensitivity regression for existing filters built before this change.)

3. src/components/BrowserFilter/FilterRow.react.js — render RegexOptionsButton for the whole family, not just matches:

Whatever the current guard is (something like constraint === 'matches' just above Line 222), widen it to check ['matches', 'starts', 'ends', 'contains'].includes(constraint).

Net effect:

  • contains substring, starts with, ends with all stay literal/escaped by default — no regex knowledge required, no risk of a stray . or ( breaking the query (the exact problem in #2153).
  • All three gain the same gear-icon flag toggles (i, u, m, x, s) that matches regex already has — solving the "no /u for emoji" gap you raised, and the general "hard-coded, arbitrary limitation" concern from #2991.
  • matches regex is untouched — still the raw/power-user escape hatch.

If this direction looks right to you, I'm ready to fold it directly into this PR's scope (updating the three files above) rather than opening a separate follow-up — just say the word and I'll kick off the planning.


Failed to handle agent chat message. Please try again.

@mtrezza

mtrezza commented Jul 10, 2026

Copy link
Copy Markdown
Member

@coderabbitai Should the strings entered in starts, ends, contains be automatically regex-escaped? Is that the current behavior?

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
F="node_modules/parse/lib/node/ParseQuery.js"
echo "---startsWith/endsWith/contains source (confirm quote usage)---"
rg -n -B2 -A8 "^\s*startsWith\s*\(|^\s*endsWith\s*\(|^\s*contains\s*\(" "$F"
echo "---quote function---"
rg -n -B2 -A12 "function quote" "$F"
echo "---dashboard queryFromFilters.js starts/ends/contains cases---"
rg -n -B2 -A6 "case 'starts'|case 'ends'|case 'contains'" src/lib/queryFromFilters.js

Length of output: 898


🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -a ParseQuery.js node_modules 2>/dev/null | head -5
echo "---fallback: search anywhere---"
fd -a "ParseQuery.js" / 2>/dev/null | head -5
echo "---dashboard queryFromFilters.js starts/ends/contains cases---"
rg -n -B2 -A6 "case 'starts'|case 'ends'|case 'contains'" src/lib/queryFromFilters.js

Length of output: 633


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/lib/queryFromFilters.js

Length of output: 10290


@mtrezza Confirmed directly in the current code — Line 200 to Line 211 and Line 235 to Line 237 of src/lib/queryFromFilters.js:

case 'starts':
  ...
  query.startsWith(field, compareTo);
  break;
case 'ends':
  query.endsWith(field, compareTo);
  break;
...
case 'contains':
  query.contains(field, String(compareTo));
  break;

None of these three cases do any manual escaping in the dashboard code itself — they pass the raw user-typed string straight to the Parse JS SDK's startsWith(), endsWith(), and contains() methods.

But that doesn't mean the input is treated as regex. As I verified earlier from node_modules/parse/lib/node/ParseQuery.js:

  • startsWith(key, prefix)this.matches(key, '^' + quote(prefix))
  • endsWith(key, suffix)this.matches(key, quote(suffix) + '$')
  • contains(key, substring)this._addCondition(key, '$regex', quote(substring))
  • quote(s) wraps the string in MongoDB's \Q...\E literal-quote syntax, which tells the regex engine to treat everything between \Q and \E as literal characters, metacharacters included.

So yes — this is already the current behavior: starts with, ends with, and contains substring are all auto-escaped today, just at the SDK layer rather than in dashboard code. A user can type a(b or c.d into any of these three fields and it's matched literally, no manual escaping needed on their part. Only matches regex (Line 233, query.matches(field, String(compareTo), modifiers)) passes the string through raw/unescaped — which is correct, since that's the deliberate "power user regex" escape hatch.

This also means my earlier suggestion to manually rebuild the regex-escape logic for contains inside queryFromFilters.js was unnecessary — query.contains() already does exactly that via quote(). The manual-escape route would only become necessary if we need to attach $options/modifiers to contains, since the SDK's contains() shortcut has no modifiers parameter — that's a separate, valid gap from your earlier point about /u and other flags.


✏️ Learnings added
Learnt from: mtrezza
URL: https://github.com/parse-community/parse-dashboard/pull/3392

Timestamp: 2026-07-10T19:07:35.261Z
Learning: In parse-dashboard's src/lib/queryFromFilters.js, the string constraints 'starts' (startsWith), 'ends' (endsWith), and 'contains' (query.contains) already delegate directly to the Parse JS SDK methods (ParseQuery.startsWith/endsWith/contains in node_modules/parse/lib/node/ParseQuery.js) without any manual regex-escaping in dashboard code. The SDK's underlying quote() helper wraps user input in MongoDB's \Q...\E literal-quote syntax, so these three operators are already literal/auto-escaped by default. Only the 'matches' constraint (query.matches) passes the raw string through unescaped as intended, since it's the deliberate raw-regex power-user operator. However, the SDK's contains() method does not accept a modifiers parameter, so 'contains substring' cannot currently support $options flags (e.g. case-insensitive 'i', unicode 'u') the way 'matches', and potentially 'starts'/'ends' (via modifiers argument), can.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@mtrezza

mtrezza commented Jul 11, 2026

Copy link
Copy Markdown
Member

@coderabbitai What if we add an "escape" option to the "matches regex" options list. Then we change the startsWith/endsWith/contains to be a shortcut to a version of the regex filter, with "escape" checked and disabled, so the user can't change it. So for all 3 filters we'd use the regex condition since it supports the modifiers and they are all disguised regex conditions anyway. This also solves the issue that startsWith/endsWith/contains don't currently allow modifiers.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -a "RegexOptionsButton" src
echo "---RegexOptionsButton full---"
F=$(fd -a "RegexOptionsButton.react.js" src | head -1)
cat -n "$F"
echo "---Filter.react.js full---"
cat -n src/components/Filter/Filter.react.js
echo "---FilterRow.react.js relevant---"
rg -n -B5 -A20 "RegexOptionsButton" src/components/BrowserFilter/FilterRow.react.js

Length of output: 741


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---grep RegexOptionsButton---"
rg -n "RegexOptionsButton" src -l
echo "---list components dirs---"
fd -a "FilterRow" src
fd -a "Filter.react.js" src
echo "---cat Filter.react.js---"
cat -n src/components/Filter/Filter.react.js 2>/dev/null || true
echo "---cat FilterRow.react.js---"
FR=$(fd -a "FilterRow.react.js" src | head -1)
echo "FR=$FR"
cat -n "$FR" 2>/dev/null || true

Length of output: 27409


@mtrezza I looked at the concrete wiring before answering, and I think the "escape as a checked+disabled modifier flag" framing introduces a problem worth flagging before we build it.

The i/u/m/x/s modifiers are not the same kind of thing as "escape." Confirmed in RegexOptionsButton (src/components/BrowserFilter/FilterRow.react.js, Line 26-171): toggleModifier just concatenates/removes single characters in the modifiers string, and that string is forwarded verbatim as MongoDB's $options on the $regex condition — they're real server-side regex-engine flags. "Escape," by contrast, is a dashboard-side preprocessing step (metachar-escaping the input string before the regex pattern is even built) — there is no $options character for it, and it must never reach the server as part of that string or the regex compile would error.

So making "escape" a checkbox living in the same modifiers value as i/u/m/x/s means:

  1. Inventing a fake flag character with no server meaning.
  2. Special-casing it in changeConstraint/toggleModifier to force-check and grey it out per constraint.
  3. Stripping it back out in queryFromFilters.js before the remaining characters are sent as real $options.

That's not less branching than today's per-constraint switch in queryFromFilters.js — it just relocates the special-casing into the modifiers layer and UI checkbox logic, at the cost of a checkbox that's misleading (it looks like a server option but isn't).

Leaner alternative that gets you the same end result: Keep starts/ends/contains/matches as four distinct, self-explanatory constraint names in the dropdown (no checkbox needed to convey "this one is literal" — the label already says "contains substring"). Internally, unify all four onto one code path:

// queryFromFilters.js
const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

switch (constraint) {
  case 'matches':
    query.matches(field, String(compareTo), modifiers);
    break;
  case 'starts':
    query.matches(field, '^' + escapeRegex(String(compareTo)), modifiers);
    break;
  case 'ends':
    query.matches(field, escapeRegex(String(compareTo)) + '$', modifiers);
    break;
  case 'contains':
    query.matches(field, escapeRegex(String(compareTo)), modifiers);
    break;
}

And on the UI side, just drop the currentConstraint === 'matches' gate at FilterRow.react.js Line 214 so RegexOptionsButton (and its i/u/m/x/s checkboxes) render for all four String constraints, not only matches.

Net effect is identical to what you want — one unified regex-family implementation, modifiers available everywhere, literal/no-escaping-required-by-user preserved for starts/ends/contains — but without a UI checkbox that doesn't correspond to anything the server actually receives.

If you still want the visible "escape" checkbox for transparency (so users explicitly see "this operator is secretly a regex under the hood"), I can build that instead — it's not wrong, just more moving parts for the same behavior. Let me know which direction you want, and I'll kick off `` for whichever one you pick — including the default-i-checked-by-default question from your earlier message, which is still unresolved either way.


Failed to handle agent chat message. Please try again.

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.

Undocumented regexp matching is confusing

2 participants