Skip to content

fix!: harden filter validation and adapter state preservation - #766

Merged
tada5hi merged 2 commits into
masterfrom
fix/validation-adapter-hardening
Jul 16, 2026
Merged

fix!: harden filter validation and adapter state preservation#766
tada5hi merged 2 commits into
masterfrom
fix/validation-adapter-hardening

Conversation

@tada5hi

@tada5hi tada5hi commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Follow-up to #763 / #765: fixes the confirmed findings from a deep code review of 51a906a.

Security / correctness

  • Filter validation (@rapiq/core):
    • A compound whose every leaf the validate hook rejects is now pruned entirely — previously it survived as an empty Filters node, bypassing the schema-defaults fallback (a client could strip a server-mandated default scope by wrapping filters in and()/or()) and crashing schema-aware expression encoding with filters:compound:empty.
    • $elemMatch interiors now run through the validate hook inside-out; previously interior conditions (e.g. a forbidden password filter) bypassed the hook entirely.
    • The Validator type no longer admits void: an inspect-only hook must return the filter — the previous docs example silently rejected every client filter. Validation also short-circuits when no hook is configured.
  • BaseParser.expandObject: dotted keys and nested objects sharing a prefix ({'realm.id': 1, realm: {name}}) now merge; previously the later key silently replaced the earlier subtree, order-dependently dropping filters.
  • @rapiq/typeorm:
    • Filter parameters bind under a per-run namespace (:rapiq_<n>_<i>); positional :0 names could rebind caller-owned bindings (TypeORM parameters are builder-global, last write wins) or a previous run's clauses — silently wrong result sets.
    • Queries without sorts/pagination leave caller-owned ORDER BY/take/skip untouched, extending the WHERE preservation contract from fix!: harden v2 beta release #763 to all sub-adapters (orderBy({}) used to wipe a caller baseline; take(undefined) erased safety caps).
  • @rapiq/sql:
    • regex values that are neither RegExp nor string (e.g. cross-realm RegExps) throw a typed AdapterError instead of being bound raw, matching @rapiq/memory.
    • buildRelationAlias is bounded to 63 chars with an FNV-1a hash suffix — PostgreSQL truncates identifiers at 63 bytes, which could silently collapse long distinct paths back onto one alias.

Consistency / infrastructure

  • One MAX_TRAVERSAL_DEPTH constant exported from core, consumed by the schema resolver and the expression/mongo parsers (the three private copies had already drifted in comparison semantics); buildFiltersDefaults moved to core beside applyFiltersSchemaValidation (was copied in three dialects).
  • Sync/async twin methods share extracted front-ends (prepareQueryContext, parseSource, prepare) so the pipelines can no longer drift.
  • Branch/PR CI now runs test:coverage: coverage thresholds previously fired for the first time in the release workflow between release-please tagging and npm publish, stranding a release with tags but no published packages.
  • Docs: validator return/idempotency semantics, TypeORM preservation contract, corrected migration note on SQL regex validation; spec placement now mirrors src/.

BREAKING CHANGE: Validator returns MaybeAsync<IFilter | undefined> (no void); relation aliases longer than 63 chars gain a hash suffix; TypeORM filter parameters are named rapiq_<n>_<i> instead of 0, 1, ….

Summary by CodeRabbit

  • New Features

    • Filter validation now supports transforming or rejecting individual conditions, including nested $elemMatch filters.
    • Rejected nested filters are pruned and schema defaults are applied when appropriate.
    • Async filter validation is supported through asynchronous parsing.
    • Recursive filter processing now consistently limits nesting depth.
  • Bug Fixes

    • Improved merging of dotted and nested filter keys.
    • Regex filters now validate input types and provide clearer errors.
    • TypeORM queries preserve existing parameters, sorting, and pagination when not overridden.
    • Long relation aliases remain unique and database-compatible.
  • Documentation

    • Expanded guidance for validation, transport encoding, and TypeORM query behavior.

Validation: fully-rejected compounds prune away (schema defaults apply
again for nested input), elemMatch interiors run through the validate
hook inside-out, and validation short-circuits when no hook is
configured. The Validator type no longer admits a void return, so
inspect-only hooks must return the filter instead of silently
rejecting every leaf.

Parsers: dotted keys and nested objects sharing a prefix merge instead
of order-dependently replacing each other; the traversal depth cap and
the defaults fallback are owned by @rapiq/core and shared by the
resolver and the expression/mongo dialects; sync/async parse pipelines
share one front-end per parser.

TypeORM: filter parameters bind under a per-run namespace so
caller-owned bindings (or a previous run) are never rebound; queries
without sorts or pagination leave caller-owned ORDER BY/take/skip
untouched, mirroring the WHERE preservation contract.

SQL: regex values that are neither RegExp nor string throw a typed
AdapterError instead of binding raw; the default relation alias is
bounded to 63 characters with a hash suffix so database identifier
truncation cannot collapse long distinct paths.

CI: coverage thresholds are enforced in branch/PR CI instead of first
failing between release-please tagging and npm publish.

BREAKING CHANGE: the filters Validator type returns
MaybeAsync<IFilter | undefined> (no void); relation aliases longer
than 63 characters gain a hash suffix; TypeORM filter parameters are
named rapiq_<n>_<i> instead of positional 0, 1, ...
Copilot AI review requested due to automatic review settings July 16, 2026 20:08

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tada5hi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1cebe531-6b30-42f0-90e4-88485673b202

📥 Commits

Reviewing files that changed from the base of the PR and between cf78cb6 and ffb0914.

📒 Files selected for processing (4)
  • packages/core/src/parser/base.ts
  • packages/parser-simple/test/unit/parser/filters.spec.ts
  • packages/typeorm/src/adapter/pagination.ts
  • packages/typeorm/test/unit/adapter/module.spec.ts
📝 Walkthrough

Walkthrough

The PR hardens filter validation and defaults across dialects, centralizes traversal limits, merges dotted filter keys, validates SQL regex inputs, bounds relation aliases, preserves TypeORM builder state and parameter bindings, updates migration documentation, and runs CI tests with coverage.

Changes

Filter parsing and validation hardening

Layer / File(s) Summary
Shared parser contracts and traversal limits
packages/core/src/constants.ts, packages/core/src/parser/*, packages/core/src/schema/*
Traversal depth and validator contracts are centralized; query context, relation gating, dotted-key expansion, and schema resolution use the shared behavior.
Inside-out schema validation and defaults
packages/core/src/parser/parameter/filters/*, packages/core/test/unit/parser/parameter/filters/*, packages/docs/guide/filters.md, packages/docs/packages/codec-url.md
Validators can replace or reject leaves, $elemMatch interiors are validated first, empty compounds are pruned, and defaults apply when no validated tree remains.
Dialect parser integration and coverage
packages/parser-expression/*, packages/parser-mongo/*, packages/parser-simple/*
Expression, Mongo, and simple parsers share traversal/default behavior and cover nested pruning, $elemMatch, absent input, and dotted-key merging.

SQL expression and relation alias hardening

Layer / File(s) Summary
SQL alias generation and regex validation
packages/sql/src/*, packages/sql/test/unit/*, .agents/migration-notes.md
Long relation aliases are deterministically hashed within 63 characters, and invalid regex value types raise typed adapter errors.

TypeORM query-builder preservation

Layer / File(s) Summary
Namespaced bindings and preserved builder state
packages/typeorm/src/adapter/*, packages/typeorm/test/unit/adapter/*, packages/docs/guide/executing-queries.md, packages/docs/packages/typeorm.md, .agents/migration-notes.md
Filter parameters use per-run namespaces, while absent sorts and pagination preserve caller-owned query-builder bindings and state.

CI coverage enforcement

Layer / File(s) Summary
Coverage test execution
.github/workflows/main.yml
The CI tests job runs coverage-enabled tests and documents threshold checks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • tada5hi/rapiq#763: Covers closely related filter-validator and relation-alias hardening.
  • tada5hi/rapiq#754: Overlaps with the TypeORM query-builder execution and parameter-binding changes.
  • tada5hi/rapiq#751: Shares Mongo $elemMatch parsing and schema-validation behavior.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: hardening filter validation and preserving adapter state.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/validation-adapter-hardening

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@packages/core/src/parser/base.ts`:
- Around line 72-83: Update expandObject to track traversal depth and enforce
the shared MAX_TRAVERSAL_DEPTH limit before recursively processing nested
objects. Reject or stop traversal consistently when the limit is exceeded, while
preserving existing path expansion for valid input; add coverage confirming
inputs deeper than the shared limit are rejected.

In `@packages/typeorm/src/adapter/pagination.ts`:
- Around line 25-31: Update the pagination handling around the queryBuilder.take
and queryBuilder.skip calls to use nullish fallback instead of falsy fallback,
preserving explicit 0 values while still converting null to undefined. Keep the
existing undefined guards and apply the same fix to both this.limit and
this.offset.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad67965d-0521-4a6a-8b43-49610451996b

📥 Commits

Reviewing files that changed from the base of the PR and between b7a7f41 and cf78cb6.

📒 Files selected for processing (29)
  • .agents/migration-notes.md
  • .github/workflows/main.yml
  • packages/core/src/constants.ts
  • packages/core/src/parser/base.ts
  • packages/core/src/parser/parameter/filters/validate.ts
  • packages/core/src/parser/query.ts
  • packages/core/src/schema/parameter/filters/schema.ts
  • packages/core/src/schema/parameter/filters/types.ts
  • packages/core/src/schema/resolver/module.ts
  • packages/core/test/unit/parser/parameter/filters/validate.spec.ts
  • packages/docs/guide/executing-queries.md
  • packages/docs/guide/filters.md
  • packages/docs/packages/codec-url.md
  • packages/docs/packages/typeorm.md
  • packages/parser-expression/src/parameter/filters/module.ts
  • packages/parser-expression/test/unit/parser/filters.spec.ts
  • packages/parser-mongo/src/parameter/filters/module.ts
  • packages/parser-mongo/test/unit/parser/filters.spec.ts
  • packages/parser-simple/src/parameter/filters/module.ts
  • packages/parser-simple/test/unit/parser/filters.spec.ts
  • packages/sql/src/helpers/relation-alias.ts
  • packages/sql/src/visitor/filters.ts
  • packages/sql/test/unit/helpers/relation-alias.spec.ts
  • packages/sql/test/unit/interpreters/regex.spec.ts
  • packages/typeorm/src/adapter/filters.ts
  • packages/typeorm/src/adapter/pagination.ts
  • packages/typeorm/src/adapter/sort.ts
  • packages/typeorm/test/unit/adapter/filters.spec.ts
  • packages/typeorm/test/unit/adapter/module.spec.ts

Comment thread packages/core/src/parser/base.ts
Comment thread packages/typeorm/src/adapter/pagination.ts
expandObject enforces the shared MAX_TRAVERSAL_DEPTH across nested
objects and dotted keys, so a crafted deeply nested (or cyclic) filter
document fails with a typed ParseError instead of overflowing the call
stack — mirroring the mongo parser's traversal cap.

The typeorm pagination adapter coalesces nullish instead of falsy
values: an explicit limit/offset of 0 is applied as a value rather
than clearing the builder's take/skip.
@tada5hi
tada5hi merged commit 97e11d4 into master Jul 16, 2026
4 checks passed
This was referenced Jul 16, 2026
@tada5hi
tada5hi deleted the fix/validation-adapter-hardening branch July 27, 2026 07:53
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.

2 participants