feat(parser-mongo): mongodb query parser dialect - #751
Conversation
…factories Mirrors the AdapterError/BuildError factories on ParseError so parser dialects can throw typed operator/feature errors; first consumer is the mongo dialect's always-throw grammar class.
visitFilterElemMatch reset the adapter's field prefix to the current
elemMatch field instead of appending to the active prefix, so a nested
elemMatch interior bound relative to the root (registering relation
"parts") instead of the enclosing element path ("items.parts").
MongoFiltersParser parses MongoDB-style filter documents ($and/$or compounds, operator objects, $not/$nor via De Morgan negation, $regex/$options, $elemMatch) into the Filters AST, schema-validated through ResolutionScope; MongoParser composes it with the simple dialect parsers for the other four parameters. Grammar errors always throw typed FiltersParseError, field/allow-list failures follow the schema failure policy. Includes the integrations/mongo docs page and the ucast reference mapping.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (38)
📝 WalkthroughWalkthroughThis PR adds ChangesMongo Parser Feature
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant MongoParser
participant MongoFiltersParser
participant SchemaRegistry
Caller->>MongoParser: parse(query, schema)
MongoParser->>MongoFiltersParser: parse(filters, options)
MongoFiltersParser->>SchemaRegistry: resolve field / relation scope
SchemaRegistry-->>MongoFiltersParser: allowed / dropped / rejected
MongoFiltersParser-->>MongoParser: Query AST
MongoParser-->>Caller: parsed query
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a new MongoDB-style filters dialect to rapiq by introducing @rapiq/parser-mongo, expanding core parse-error factories, fixing nested $elemMatch prefix handling in the SQL visitor, and documenting the dialect and its failure model in the docs site.
Changes:
- Introduce
@rapiq/parser-mongowith aMongoFiltersParser(MongoDB-like filter documents → rapiqFiltersAST) and aMongoParserthat composes simple parsers for non-filter parameters. - Extend core parsing errors with
ParseError.operatorUnsupported(...)andParseError.featureUnsupported(...)factories. - Fix SQL nested
elemMatchfield-prefix composition and add unit coverage for the nested case; add docs pages + navigation updates.
Reviewed changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds @rapiq/parser-mongo to the package overview table. |
| packages/sql/test/unit/interpreters/elem-match.spec.ts | Adds a regression test for nested elemMatch join-path composition. |
| packages/sql/src/visitor/filters.ts | Fixes visitFilterElemMatch to compose (not reset) the adapter field prefix. |
| packages/parser-mongo/tsdown.config.ts | Adds tsdown build config for the new package. |
| packages/parser-mongo/tsconfig.json | Adds package TS config (incl. vitest globals typing for tests). |
| packages/parser-mongo/tsconfig.build.json | Adds build-only TS config for the new package. |
| packages/parser-mongo/test/vitest.config.ts | Adds vitest config and coverage settings for the new package. |
| packages/parser-mongo/test/unit/parser/parser.spec.ts | Adds end-to-end MongoParser unit tests over full query inputs. |
| packages/parser-mongo/test/unit/parser/filters.spec.ts | Adds extensive unit coverage for Mongo filter grammar/semantics, schema policy, defaults, and typing surface. |
| packages/parser-mongo/test/data/type.ts | Adds shared test entity types used by parser specs. |
| packages/parser-mongo/test/data/schema.ts | Adds a schema registry used by parser tests (allow-lists, mappings, defaults). |
| packages/parser-mongo/test/data/index.ts | Barrel export for parser-mongo test data. |
| packages/parser-mongo/src/parameter/sorts/module.ts | Introduces MongoSortParser (currently delegating to simple dialect). |
| packages/parser-mongo/src/parameter/sorts/index.ts | Exports mongo sorts parser module. |
| packages/parser-mongo/src/parameter/relations/module.ts | Introduces MongoRelationsParser (delegating to simple dialect). |
| packages/parser-mongo/src/parameter/relations/index.ts | Exports mongo relations parser module. |
| packages/parser-mongo/src/parameter/pagination/module.ts | Introduces MongoPaginationParser (delegating to simple dialect). |
| packages/parser-mongo/src/parameter/pagination/index.ts | Exports mongo pagination parser module. |
| packages/parser-mongo/src/parameter/index.ts | Barrel export for all mongo parameter parsers. |
| packages/parser-mongo/src/parameter/filters/types.ts | Defines typed input surface for mongo filter documents (MongoFiltersParserInput, operator object types). |
| packages/parser-mongo/src/parameter/filters/module.ts | Implements MongoFiltersParser (Mongo-style document walk, operator validation, De Morgan negation, $elemMatch, schema policy integration). |
| packages/parser-mongo/src/parameter/filters/index.ts | Barrel export for filters parser, constants, and types. |
| packages/parser-mongo/src/parameter/filters/constants.ts | Defines supported/unsupported mongo operator vocabulary and AST mappings. |
| packages/parser-mongo/src/parameter/fields/module.ts | Introduces MongoFieldsParser (delegating to simple dialect). |
| packages/parser-mongo/src/parameter/fields/index.ts | Exports mongo fields parser module. |
| packages/parser-mongo/src/module.ts | Adds MongoParser composing mongo filters + simple parsers for other parameters. |
| packages/parser-mongo/src/index.ts | Package entry-point exports for mongo parser and parameters. |
| packages/parser-mongo/README.md | Adds package-level README describing usage, semantics, and docs link. |
| packages/parser-mongo/package.json | Adds the new workspace package manifest and scripts. |
| packages/docs/integrations/mongo.md | Adds the Mongo parser documentation (operators, deviations, failure model, usage, errors). |
| packages/docs/integrations/index.md | Links the Mongo parser docs from the integrations index. |
| packages/docs/guide/build.md | Updates reserved-keys note to point to the Mongo parser documentation. |
| packages/docs/getting-started/installation.md | Adds @rapiq/parser-mongo to installation guidance. |
| packages/docs/.vitepress/config.mjs | Adds “Mongo Parser” to the docs sidebar navigation. |
| packages/core/src/errors/parse.ts | Adds ParseError.operatorUnsupported and ParseError.featureUnsupported factories. |
| package-lock.json | Adds workspace lock entries for @rapiq/parser-mongo. |
| .agents/structure.md | Updates package inventory + layer notes to include parser-mongo. |
| .agents/references/ucast.md | Adds a reference note on ucast and how it maps to rapiq’s mongo dialect approach. |
| .agents/architecture.md | Updates architecture notes to document the new mongo parser dialect and reserved keys. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
🧹 Nitpick comments (3)
packages/sql/src/visitor/filters.ts (1)
89-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider restoring prefix in a
finallyblock.If
expr.value.accept(this)throws,setFieldPrefix(oldPrefix)(line 96) never runs, leaving the adapter's field prefix in a corrupted composed state for any subsequent use of the same adapter instance. Since the fix now makes prefixes compose across nesting levels, a stale un-restored prefix is more error-prone to diagnose than before.♻️ Suggested fix
visitFilterElemMatch(expr: Filter<FilterFieldOperator.ELEM_MATCH, Filter | Filters>): IFiltersAdapter { const oldPrefix = this.adapter.getFieldPrefix(); this.adapter.setFieldPrefix(`${oldPrefix}${expr.field}.`); - expr.value.accept(this); - - this.adapter.setFieldPrefix(oldPrefix); + try { + expr.value.accept(this); + } finally { + this.adapter.setFieldPrefix(oldPrefix); + } return this.adapter; }🤖 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 `@packages/sql/src/visitor/filters.ts` around lines 89 - 99, The field prefix in visitFilterElemMatch is not safely restored if expr.value.accept(this) throws, which can leave the shared adapter state corrupted. Update visitFilterElemMatch in filters.ts to restore the previous prefix inside a finally block around expr.value.accept(this), using oldPrefix and this.adapter.setFieldPrefix so the prefix is always reset even on errors.packages/parser-mongo/test/unit/parser/parser.spec.ts (1)
125-129: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
toThrow(error)matches by message, not by reference/type.Vitest's
toThrowwith anErrorinstance argument asserts message equality only, so this test would still pass even if the thrown error were a different error class with the same message. Consider asserting the error type as well (e.g.toThrow(FiltersParseError)combined with a message check, or catching and checkinginstanceof) to more precisely lock in the "always throws typedFiltersParseError" contract described in the PR objectives.♻️ Suggested strengthening of the assertion
- expect(() => parser.parse({ filters: { $where: 'this.a > 1' } }, { schema: 'user' })).toThrow(error); + expect(() => parser.parse({ filters: { $where: 'this.a > 1' } }, { schema: 'user' })) + .toThrowError(expect.objectContaining({ name: error.name, message: error.message }));🤖 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 `@packages/parser-mongo/test/unit/parser/parser.spec.ts` around lines 125 - 129, The assertion in parser.spec.ts only checks error message equality via parser.parse and toThrow(error), so it can miss the wrong error type. Strengthen the test around parser.parse, FiltersParseError.operatorUnsupported, and the full-query filter grammar case by asserting the thrown error is a FiltersParseError type as well as matching the expected message, using either toThrow(FiltersParseError) plus a message check or a try/catch with instanceof.packages/parser-mongo/src/parameter/filters/module.ts (1)
601-641: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winUser-controlled
$regexpattern feedsnew RegExp.Static analysis flags
new RegExp(value, flags)as ReDoS-prone sincevalueis client-supplied. This file only constructs the RegExp for syntax validation, but the resulting pattern is stored on theFilterand will eventually be evaluated downstream (SQL/in-memory visitor) against real data — an attacker can still submit a catastrophic-backtracking pattern (e.g.(a+)+$) through$regex/$options.Consider documenting this risk for consumers of the dialect (untrusted
$regexinput should be treated as executable), and/or adding a cheap safeguard such as a maximum pattern length or a complexity check before constructing the RegExp.🤖 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 `@packages/parser-mongo/src/parameter/filters/module.ts` around lines 601 - 641, The validateRegex() path accepts user-controlled $regex/$options and directly constructs a RegExp, which can carry ReDoS risk into later Filter evaluation. Update validateRegex() in the parser-mongo filters module to either add a lightweight safeguard before new RegExp(value, flags) (for example a maximum pattern length or simple complexity guard) or explicitly document in this code path that untrusted regex input is dangerous and must be constrained by consumers. Keep the checks aligned with the existing RegExp handling and keyValueInvalid / syntaxInvalid error flow.Source: Linters/SAST tools
🤖 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.
Nitpick comments:
In `@packages/parser-mongo/src/parameter/filters/module.ts`:
- Around line 601-641: The validateRegex() path accepts user-controlled
$regex/$options and directly constructs a RegExp, which can carry ReDoS risk
into later Filter evaluation. Update validateRegex() in the parser-mongo filters
module to either add a lightweight safeguard before new RegExp(value, flags)
(for example a maximum pattern length or simple complexity guard) or explicitly
document in this code path that untrusted regex input is dangerous and must be
constrained by consumers. Keep the checks aligned with the existing RegExp
handling and keyValueInvalid / syntaxInvalid error flow.
In `@packages/parser-mongo/test/unit/parser/parser.spec.ts`:
- Around line 125-129: The assertion in parser.spec.ts only checks error message
equality via parser.parse and toThrow(error), so it can miss the wrong error
type. Strengthen the test around parser.parse,
FiltersParseError.operatorUnsupported, and the full-query filter grammar case by
asserting the thrown error is a FiltersParseError type as well as matching the
expected message, using either toThrow(FiltersParseError) plus a message check
or a try/catch with instanceof.
In `@packages/sql/src/visitor/filters.ts`:
- Around line 89-99: The field prefix in visitFilterElemMatch is not safely
restored if expr.value.accept(this) throws, which can leave the shared adapter
state corrupted. Update visitFilterElemMatch in filters.ts to restore the
previous prefix inside a finally block around expr.value.accept(this), using
oldPrefix and this.adapter.setFieldPrefix so the prefix is always reset even on
errors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1b2e40a3-218e-41a7-9c88-c7172b6cb3ff
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (38)
.agents/architecture.md.agents/references/ucast.md.agents/structure.mdREADME.mdpackages/core/src/errors/parse.tspackages/docs/.vitepress/config.mjspackages/docs/getting-started/installation.mdpackages/docs/guide/build.mdpackages/docs/integrations/index.mdpackages/docs/integrations/mongo.mdpackages/parser-mongo/README.mdpackages/parser-mongo/package.jsonpackages/parser-mongo/src/index.tspackages/parser-mongo/src/module.tspackages/parser-mongo/src/parameter/fields/index.tspackages/parser-mongo/src/parameter/fields/module.tspackages/parser-mongo/src/parameter/filters/constants.tspackages/parser-mongo/src/parameter/filters/index.tspackages/parser-mongo/src/parameter/filters/module.tspackages/parser-mongo/src/parameter/filters/types.tspackages/parser-mongo/src/parameter/index.tspackages/parser-mongo/src/parameter/pagination/index.tspackages/parser-mongo/src/parameter/pagination/module.tspackages/parser-mongo/src/parameter/relations/index.tspackages/parser-mongo/src/parameter/relations/module.tspackages/parser-mongo/src/parameter/sorts/index.tspackages/parser-mongo/src/parameter/sorts/module.tspackages/parser-mongo/test/data/index.tspackages/parser-mongo/test/data/schema.tspackages/parser-mongo/test/data/type.tspackages/parser-mongo/test/unit/parser/filters.spec.tspackages/parser-mongo/test/unit/parser/parser.spec.tspackages/parser-mongo/test/vitest.config.tspackages/parser-mongo/tsconfig.build.jsonpackages/parser-mongo/tsconfig.jsonpackages/parser-mongo/tsdown.config.tspackages/sql/src/visitor/filters.tspackages/sql/test/unit/interpreters/elem-match.spec.ts
|
@coderabbitai full review |
✅ Action performedFull review finished. |
Implements issue #701: a new parser dialect accepting MongoDB-style filter documents, designed against the ucast reference implementation (mapped in
.agents/references/ucast.md). Dialect semantics were settled by a design review across three lenses (mongo fidelity, IR consistency, consumer DX); decisions recorded in local plan 013.Design decisions
$-operators, malformed operator values, invalid compound arrays, mixed operator/plain keys, non-object input, nesting deeper than 32 levels — always throw typedFiltersParseError, independent ofthrowOnFailure. A$-prefixed key is never a field name, and a silently dropped$whereor typo'd operator would mean an unfiltered result set. Field-key/allow-list failures stay schema-governed (drop by default, throw underthrowOnFailure), exactly like the simple dialect.$not/$nordesugar via De Morgan operator negation ($eq↔$ne,$lt↔$gte,$in↔$nin, AND ↔ OR,$existsflips its value) — the expression dialect'snot(...)precedent; the AST deliberately has no NOT compound. Non-negatable operators ($regex/$mod/$elemMatch) under negation throwOPERATOR_UNSUPPORTED.$startsWith/$notStartsWith/$endsWith/$notEndsWith/$contains/$notContainsare accepted as documented rapiq extensions ($+FilterFieldOperatorvalue, aligned with the build layer's$-vocabulary from plan 012) — the only dialect covering the entire operator inventory, so a future mongo codec's encode is total under the subset law.{ realm: { name: 'x' } }≡{ 'realm.name': 'x' }); bare arrays mean$in; algebraic negation does not replicate mongo's missing-field semantics;$elemMatchsupports the nested-document form only (the IR has no element self-reference marker);$where/$size/$all/$typeand the evaluation/geo/bitwise operators throw.$regexaccepts bare RegExp,$regex: RegExp, and$regex: string+$options(key-order independent); the AST value is always aRegExpinstance (the SQL visitor dereferences.source/.ignoreCase). Dangling$optionsand$optionsbeside a RegExp-valued$regexthrow.Dateinstances pass through. Strict per-operator argument validation throws unconditionally.Changes
@rapiq/parser-mongo(new)MongoFiltersParser: document walk (implicit AND, explicit$and/$or/$noralways materialized, nested compounds preserved — no flattening), operator objects,$elemMatchwith schema descend (related schema viaschemaMapping, unbound fallback for schemaless array columns, relations gating respected), schema defaults on absent/{}/all-dropped input, recursion depth cap (crafted ~17KB nested-$andJSON previously escaped as an untypedRangeError).MongoParsercomposing the filters parser with the simple-dialect parsers for fields/relations/pagination/sort (parser-expression template).parseTyped(input: MongoFiltersParserInput<RECORD>)— field keys viaNestedKeys, per-path operator objects,$nottyped to the negatable subset.$elemMatchscoping, schema policy/strict/defaults, grammar always-throws set, hostile inputs).@rapiq/coreParseError.operatorUnsupported(...)/ParseError.featureUnsupported(...)factories (existingErrorCodemembers), mirroringAdapterError/BuildError.@rapiq/sqlvisitFilterElemMatchreset the adapter field prefix instead of composing it, so a nestedelemMatchinterior bound relative to the root (registering relationpartsinstead ofitems.parts). Newly client-reachable through this dialect; pre-existing for hand-built ASTs.Docs
integrations/mongo.md(operator table with extension markers, deviations callout, failure model, usage) + sidebar, integrations index, installation, root README package table,.agents/{structure,architecture}.mdupdated (the "$and/$orreserved for a future mongo parser dialect" note now points here),guide/build.mdreserved-keys note linked.Closes #701
Summary by CodeRabbit
@rapiq/parser-mongopackage to parse MongoDB-stylefiltersinto the shared query AST (including$and/$or/$nor, common comparison/string operators, and$elemMatch).$elemMatchfield-prefix handling in SQL generation to produce correct nested paths.elemMatchnesting.