Skip to content

TML-2376: Mongo middleware can rewrite params before encode#652

Merged
wmadden merged 9 commits into
mainfrom
tml-2376-mongo-middleware-param-mutator-runtime-wiring-defer
May 31, 2026
Merged

TML-2376: Mongo middleware can rewrite params before encode#652
wmadden merged 9 commits into
mainfrom
tml-2376-mongo-middleware-param-mutator-runtime-wiring-defer

Conversation

@wmadden-electric
Copy link
Copy Markdown
Contributor

@wmadden-electric wmadden-electric commented May 31, 2026

Linked issue

Refs TML-2376. Restores Mongo parity with the SQL-family param-mutator seam shipped under TML-2373 (deferred out of that project's Postgres-only scope). One follow-up is intentionally left out of scope: TML-2752 (see Reviewer notes).

At a glance

A query parameter in Mongo is a MongoParamRef — a placeholder carrying the raw value plus an optional codecId tag identifying how it should be encoded. This PR lets a middleware see those refs in beforeExecute and rewrite their values before they're encoded and sent to the driver:

const middleware: MongoMiddleware = {
  name: 'bulk-transform',
  async beforeExecute(_plan, _ctx, params) {
    // walk the params, pick the ones tagged for this codec
    const targets = [...(params?.entries() ?? [])].filter(
      (entry) => entry.codecId === BULK_CODEC_ID,
    );
    // one batched async call, then write the results back onto the refs
    const transformed = await bulkTransform(targets.map((e) => e.value));
    params?.replaceValues(
      targets.map((e, i) => ({ ref: e.ref, newValue: transformed[i] })),
    );
  },
};
// driver receives { token: 'bulk:alpha', note: 'plain' } — tagged token rewritten, untagged note untouched

Before this change the third params argument was inert: Mongo's lower() resolved every MongoParamRef to its raw value during lowering, so by the time beforeExecute ran the command carried only resolved primitives — there was nothing left to rewrite.

Decision

Make Mongo middleware able to rewrite parameter values before encode, by deferring value resolution until after the beforeExecute chain — the same lifecycle the SQL family already follows (ADR 215). Concretely this PR ships:

  1. A two-phase adapter lowering — the single lower() becomes structuralLower (shape only, refs preserved) + resolveParams (resolve refs → frozen wire command).
  2. A MongoParamRefMutator, wired through the runtime — constructed over the unresolved draft, handed to beforeExecute, and consulted when the draft is resolved.
  3. A fail-loud safety guard — because the plan briefly holds an unresolved command during beforeExecute, content-hashing it then is a misuse; the hash helper now throws a clear error instead of failing cryptically.

How it fits together

The work builds up in four steps; the execute pipeline ends up as structuralLower → mutator → beforeExecute → resolveParams → driver.

  1. Split resolution out of lowering (mongo-adapter, mongo-lowering). structuralLower does every structural transform — collection, filter shape, pipeline stages, update shape — but leaves MongoParamRef nodes in place. The deferred resolveParams pass produces the frozen AnyMongoWireCommand. Resolution previously lived in two places (#resolveDocument and lowerFilter, the latter reached from many command paths plus $match / $geoNear / $lookup / $merge); all of it moved into the resolve phase together, so nothing resolves early.
  2. Build the mutator over the draft (7-runtime). flattenMongoParamRefs yields every MongoParamRef leaf regardless of nesting (object / array / filter / pipeline); createMongoParamRefMutator exposes the SQL-shaped entries() / replaceValue / replaceValues surface. It keeps a reference-identity fast path: when nothing is replaced, currentDraft() returns the original draft by reference, so the common no-mutation path allocates no second tree.
  3. Re-order the execute pipeline (mongo-runtime.ts) to construct the mutator, run the chain, then resolve currentDraft(). The content-hash call site moves after resolveParams, so the digest reflects any mutation and never feeds a MongoParamRef instance to canonicalStringify (which rejects class instances).
  4. Make the transitional state safe. Between structural-lower and resolve, the plan's command slot holds the unresolved draft — typed as the resolved wire command (a known trade-off; see Reviewer notes). The supported surface for middleware is the params mutator, not structural reads of plan.command. To stop the one dangerous misuse silently, computeMongoContentHash now throws RUNTIME.CONTENT_HASH_REQUIRES_RESOLVED_COMMAND if handed a pre-resolve draft, and the JSDoc on beforeExecute / MongoMiddlewareContext.contentHash / MongoExecutionPlan.command documents the lifecycle honestly.

Reviewer notes

  • plan.command is a deliberate transitional state, not an oversight. During beforeExecute the command slot carries the unresolved MongoLoweredDraft while typed as AnyMongoWireCommand (via a narrowed blindCast in mongo-runtime.ts). Middleware is expected to use the params mutator, never structural reads of plan.command. The footgun is closed two ways: the content-hash guard fails loud, and the contract is now documented on the relevant types. The fully type-honest fix — a distinct pre-resolve plan type for beforeExecute — needs a change to the cross-family framework middleware SPI (RuntimeMiddleware uses one TPlan across all hooks), so it's tracked separately as TML-2752 rather than folded in here.
  • Largest diff is param-ref-mutator.ts (new, ~400 lines) in 7-runtime — the mutator surface, the flatten walk, and the draft-reconstruction helper. Reconstruction rebuilds the tree from the original draft applying an identity-keyed override map (MongoParamRef is frozen, so values can't be mutated in place); replacement nodes preserve codecId / name so resolveParams still routes to the right codec.
  • flattenMongoParamRefs enumerates exactly what resolveParams resolves. Both walk plain objects and arrays and treat everything else (incl. Date) as a leaf; this parity is the invariant that keeps a ref from being invisible-to-middleware yet still resolved. It's documented on the walk and pinned by a parity test.
  • ADR 215 gains a Mongo subsection recording the lifecycle parity and two intentional SQL/Mongo asymmetries (Mongo exposes the phases on the adapter SPI and homes the mutator in 7-runtime, vs SQL keeping both runtime-/lanes-private), plus Mongo's stricter distinct-draft type.

Behavior changes & evidence

  • Mongo middleware observes unresolved MongoParamRef nodes and can rewrite them before encode. Wiring in packages/2-mongo-family/7-runtime/src/mongo-runtime.ts; mutator in packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts. Proof: packages/2-mongo-family/7-runtime/test/execute-param-mutator-wiring.test.ts asserts a bulk middleware's writeback reaches a stub driver (token: 'bulk:alpha', untagged note unchanged).
  • Resolution is deferred past the chain. packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts (two-phase split) + lowering.ts / resolve-value.ts. Proof: packages/3-mongo-target/2-mongo-adapter/test/structural-lower.test.ts pins MongoParamRef survival across all command shapes.
  • Content-hash reflects mutated, resolved values and rejects pre-resolve drafts. packages/2-mongo-family/7-runtime/src/content-hash.ts. Proof: the hash-discrimination case in execute-param-mutator-wiring.test.ts (mutating vs identity middleware → different hashes) and packages/2-mongo-family/7-runtime/test/content-hash-guard.test.ts (draft input → RUNTIME.CONTENT_HASH_REQUIRES_RESOLVED_COMMAND).

Lowering-contract audit

Lowered-command consumer Behaviour change? Notes
computeMongoContentHash Yes Hashes the resolved wire command (post-resolveParams, post-mutation), so a mutation changes the digest; now also throws on a pre-resolve draft instead of feeding MongoParamRef to canonicalStringify.
Driver execute(command) No Still receives the same frozen AnyMongoWireCommand shape; only when it's built moved after beforeExecute.
decodeMongoRow (collection read) No Reads command.collection from the resolved exec, unchanged.
runBeforeCompile / plan meta No Upstream of structural-lower, untouched.
Raw commands (no param refs) No Mutator is a no-op; resolveParams over the unchanged draft matches the prior eager lower().

Verification

  • pnpm test in packages/2-mongo-family/7-runtime — 170 passing.
  • pnpm test in packages/3-mongo-target/2-mongo-adapter — 284 passing.
  • pnpm typecheck (both packages) — clean.
  • pnpm lint:deps — clean.

Alternatives considered

  • Resolve eagerly and hand middleware the resolved primitives (status quo). Rejected: middleware can't recover the codec-tagged ref it needs to target, which is the entire point of the bulk-pattern seam.
  • Hash the pre-resolution draft. Rejected: it would route MongoParamRef class instances through canonicalStringify (which rejects them) and would key the cache on pre-mutation values, returning stale results when middleware mutates.
  • Always allocate a post-mutation tree. Rejected: the no-mutation path is the common case; the reference-identity fast path keeps it allocation-free, matching the SQL family.
  • Give beforeExecute a type-honest pre-resolve plan type now. Rejected for this PR: it's a cross-family change to the framework middleware SPI; tracked as TML-2752. The transitional cast is made safe in the meantime by the content-hash guard and documented contract.

Checklist

  • All commits are signed off (git commit -s).
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated.
  • The PR title is in TML-NNNN: <sentence-case title> form.
  • Skill update: n/a — internal framework parity; no user-facing CLI / config / error-code surface changes.

Summary by CodeRabbit

  • New Features

    • Two-phase Mongo lowering: a structural draft stage plus param-resolution, letting middleware mutate params before final encoding.
    • Param-ref mutator API to inspect and replace parameterized values in lowered commands.
  • Refactor

    • Runtime split into structural lowering and separate param resolution; lowering now preserves unresolved param refs for middleware.
  • Documentation

    • Docs and ADR updated to describe lifecycle and middleware contracts.
  • Tests

    • Extensive tests added/updated for param mutator, lowering parity, content-hash guards, and runtime behavior.

@wmadden-electric wmadden-electric requested a review from a team as a code owner May 31, 2026 09:15
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 31, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 700f2b12-f162-41dc-a9af-228c94f064c3

📥 Commits

Reviewing files that changed from the base of the PR and between b00339c and 21fb94c.

📒 Files selected for processing (1)
  • packages/2-mongo-family/7-runtime/test/content-hash-guard.test.ts

📝 Walkthrough

Walkthrough

This PR splits Mongo lowering into structural and resolution phases, adds middleware-visible param-ref mutation over lowered drafts, and updates runtime execution, adapter wiring, docs, and tests to use the new flow.

Changes

Two-Phase Lowering Architecture

Layer / File(s) Summary
Adapter contract and exported draft type
packages/2-mongo-family/6-transport/mongo-lowering/src/adapter-types.ts, packages/2-mongo-family/6-transport/mongo-lowering/src/exports/index.ts
MongoLoweredDraft is added as the intermediate lowered command shape, and MongoAdapter now exposes structuralLower(plan) and resolveParams(draft, ctx) alongside updated lower() documentation.
Param-ref mutator API and traversal
packages/2-mongo-family/7-runtime/package.json, packages/2-mongo-family/7-runtime/src/exports/index.ts, packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts
Mongo param-ref handles, entries, mutator interfaces, flattening, replacement, and draft rebuilding are added and re-exported; @prisma-next/mongo-value is promoted to a runtime dependency.
Structural lowering for filters and stages
packages/3-mongo-target/2-mongo-adapter/src/lowering.ts, packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts, packages/3-mongo-target/2-mongo-adapter/test/structural-lower.test.ts
Synchronous structural lowering is added for filter, stage, and pipeline ASTs, and the adapter now produces MongoLoweredDraft values before resolution. Tests cover param-ref preservation, raw passthrough, and lower/resolve equivalence.
Parameter resolution and draft doc hydration
packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts, packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts, packages/2-mongo-family/7-runtime/test/content-hash-guard.test.ts
Draft documents are resolved into wire commands with abort handling, update-pipeline handling, and guarded content-hash behavior for unresolved commands.
Runtime execute pipeline and middleware behavior
packages/2-mongo-family/7-runtime/src/mongo-middleware.ts, packages/2-mongo-family/7-runtime/src/mongo-runtime.ts, packages/2-mongo-family/7-runtime/src/content-hash.ts, packages/2-mongo-family/7-runtime/src/mongo-execution-plan.ts, packages/2-mongo-family/7-runtime/test/execute-param-mutator-wiring.test.ts, packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts, packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts, packages/2-mongo-family/7-runtime/test/param-ref-mutator.test.ts, packages/2-mongo-family/7-runtime/test/flatten-resolve-parity.test.ts
execute() now runs structural lowering, exposes a MongoParamRefMutator during beforeExecute, then resolves params before driver execution. Middleware context and content-hash behavior are updated, and tests cover mutation wiring, abort signals, hash changes, traversal parity, and mutator semantics.
Adapter wiring, docs, and runner mocks
packages/3-mongo-target/2-mongo-adapter/src/exports/runtime.ts, packages/3-mongo-target/1-mongo-target/test/mongo-runner.test.ts, test/integration/test/cross-package/cross-family-middleware.test.ts, packages/2-mongo-family/6-transport/mongo-lowering/README.md, docs/architecture docs/adrs/ADR 215 - Runtime middleware lifecycle beforeExecute before encodeParams.md
Runtime adapter descriptors and test mocks now bind structuralLower and resolveParams, while the README and ADR describe the two-phase Mongo lowering lifecycle and its middleware placement.

Sequence Diagram(s)

sequenceDiagram
  participant MongoRuntime as MongoRuntime.execute()
  participant MongoAdapter as MongoAdapter
  participant MongoParamRefMutator as MongoParamRefMutator
  participant BeforeExecuteChain as beforeExecute middleware
  participant MongoDriver as MongoDriver

  MongoRuntime->>MongoAdapter: structuralLower(plan)
  MongoAdapter-->>MongoRuntime: MongoLoweredDraft
  MongoRuntime->>MongoParamRefMutator: createMongoParamRefMutator(draft)
  MongoRuntime->>BeforeExecuteChain: runBeforeExecuteChain(exec, params)
  BeforeExecuteChain->>MongoParamRefMutator: entries()
  BeforeExecuteChain->>MongoParamRefMutator: replaceValue(...)
  BeforeExecuteChain-->>MongoRuntime: middleware complete
  MongoRuntime->>MongoAdapter: resolveParams(draft, ctx)
  MongoAdapter-->>MongoRuntime: AnyMongoWireCommand
  MongoRuntime->>MongoDriver: execute(command)
  MongoDriver-->>MongoRuntime: result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90 minutes

Suggested reviewers

  • wmadden

Poem

🐰 I hop through drafts before they’re done,
🌿 Leave refs unbound till middleware’s fun,
🔧 Then codecs sing when work is through,
✨ Two phases split the path in two,
🥕 A happy rabbit cheers: “All clean and new!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.92% 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 'TML-2376: Mongo middleware can rewrite params before encode' directly captures the main feature: enabling Mongo middleware to mutate parameter values before encoding, which is the central behavioral change across multiple files and the core PR objective.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2376-mongo-middleware-param-mutator-runtime-wiring-defer

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 31, 2026

size-limit report 📦

Path Size
postgres / no-emit 135.37 KB (0%)
postgres / emit 125.16 KB (0%)
mongo / no-emit 75.14 KB (+1.69% 🔺)
mongo / emit 70.15 KB (+1.83% 🔺)

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 31, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@652

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@652

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@652

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@652

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@652

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@652

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@652

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@652

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@652

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@652

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@652

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@652

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@652

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@652

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@652

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@652

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@652

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@652

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@652

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@652

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@652

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@652

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@652

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@652

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@652

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@652

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@652

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@652

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@652

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@652

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@652

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@652

prisma-next

npm i https://pkg.pr.new/prisma-next@652

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@652

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@652

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@652

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@652

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@652

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@652

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@652

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@652

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@652

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@652

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@652

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@652

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@652

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@652

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@652

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@652

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@652

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@652

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@652

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@652

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@652

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@652

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@652

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@652

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@652

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@652

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@652

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@652

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@652

commit: b00339c

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts (1)

101-105: ⚡ Quick win

Add checkAborted guard at entry for consistency.

resolveValue (line 49) and resolveDraftDoc (line 137) both check abort status at entry, but resolveDraftSlot does not. While all paths eventually reach an abort check, adding checkAborted(ctx, 'encode') here would maintain consistency and provide fail-fast behavior.

🛡️ Proposed addition
 async function resolveDraftSlot(
   value: unknown,
   codecs: MongoCodecRegistry,
   ctx: CodecCallContext,
 ): Promise<unknown> {
+  checkAborted(ctx, 'encode');
   if (value instanceof MongoParamRef) {
🤖 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/3-mongo-target/2-mongo-adapter/src/resolve-value.ts` around lines
101 - 105, resolveDraftSlot is missing an immediate abort check for consistency
with resolveValue and resolveDraftDoc; add a call to checkAborted(ctx, 'encode')
at the start of the resolveDraftSlot function so it fails fast if the context is
aborted. Locate the async function resolveDraftSlot(value, codecs, ctx) and
insert the checkAborted(ctx, 'encode') guard as the first statement to mirror
the behavior in resolveValue and resolveDraftDoc.
packages/3-mongo-target/2-mongo-adapter/src/lowering.ts (1)

469-666: ⚖️ Poor tradeoff

Consider extracting shared stage-lowering logic.

The structuralLowerStage function duplicates ~200 lines from lowerStage (lines 235-450), differing only in whether it calls synchronous structuralLowerFilter/structuralLowerPipeline vs async variants. A shared helper parameterized by filter/pipeline lowering strategies could eliminate this duplication and improve maintainability.

🤖 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/3-mongo-target/2-mongo-adapter/src/lowering.ts` around lines 469 -
666, structuralLowerStage duplicates most logic from lowerStage; extract a
shared helper (e.g., buildLowerStage) that accepts strategy functions for
lowering components (filter, pipeline, exprRecord, agg/window helpers as needed)
and reuse it from both structuralLowerStage and lowerStage by passing
structuralLowerFilter/structuralLowerPipeline (sync) or their async variants;
update structuralLowerStage to call the helper to remove the ~200-line
duplication while preserving current behavior and signatures of
structuralLowerStage and lowerStage.
packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts (1)

170-173: ⚡ Quick win

Prefer ifDefined for conditional object spreads.

Replace the imperative mutation pattern with the repo's idiomatic spread helper for cleaner, more declarative code.

♻️ Proposed refactor using `ifDefined`
+import { ifDefined } from '`@prisma-next/utils/defined`';
+
 function substituteSlot(value: unknown, overrides: ReadonlyMap<MongoParamRef, unknown>): unknown {
   if (value instanceof MongoParamRef) {
     if (overrides.has(value)) {
-      const opts: { name?: string; codecId?: string } = {};
-      if (value.name !== undefined) opts.name = value.name;
-      if (value.codecId !== undefined) opts.codecId = value.codecId;
-      return new MongoParamRef(overrides.get(value), opts);
+      return new MongoParamRef(overrides.get(value), {
+        ...ifDefined('name', value.name),
+        ...ifDefined('codecId', value.codecId),
+      });
     }

Based on learnings: prefer ifDefined from prisma-next/utils/defined for conditional object spreads instead of inline ternary-based spreads or imperative mutation.

🤖 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/2-mongo-family/7-runtime/src/param-ref-mutator.ts` around lines 170
- 173, The code mutates an opts object to conditionally include name and codecId
when constructing the MongoParamRef; replace that imperative pattern with the
repo helper ifDefined from prisma-next/utils/defined to build a single immutable
opts via spreads (e.g. const opts = { ...ifDefined(value.name, { name:
value.name }), ...ifDefined(value.codecId, { codecId: value.codecId }) }) and
then return new MongoParamRef(overrides.get(value), opts); also add the required
import for ifDefined and remove the mutable opts mutation.
🤖 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/2-mongo-family/7-runtime/src/param-ref-mutator.ts`:
- Line 366: Replace the bare double-cast usages with the typed helper blindCast
from '`@prisma-next/utils/casts`': locate the statement assigning handle (the
expression "ref as unknown as MongoParamRefHandle<string | undefined>") and the
other similar casts around the same block (also at the sites referenced by the
reviewer), and change them to use blindCast<MongoParamRefHandle<string |
undefined>, "Explain reason here">(...ref...) so the cast is explicit and
documented; import blindCast from '`@prisma-next/utils/casts`' if not already
imported and add a short reason string for the cast.
- Line 389: Replace the bare TypeScript "as" cast used for replaceValue with the
blindCast utility to satisfy the no-bare-casts rule: import blindCast (or
castAs) from "`@prisma-next/utils/casts`" if not already present, then change the
assignment that uses replaceValue as
MongoParamRefMutator<TCodecMap>['replaceValue'] to
blindCast<MongoParamRefMutator<TCodecMap>['replaceValue'], "ReplaceValue
cast">(replaceValue) so the cast is explicit and annotated; locate the
replacement in the object where replaceValue is assigned (symbol: replaceValue
and type MongoParamRefMutator<TCodecMap>['replaceValue']) and update
accordingly.

---

Nitpick comments:
In `@packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts`:
- Around line 170-173: The code mutates an opts object to conditionally include
name and codecId when constructing the MongoParamRef; replace that imperative
pattern with the repo helper ifDefined from prisma-next/utils/defined to build a
single immutable opts via spreads (e.g. const opts = { ...ifDefined(value.name,
{ name: value.name }), ...ifDefined(value.codecId, { codecId: value.codecId })
}) and then return new MongoParamRef(overrides.get(value), opts); also add the
required import for ifDefined and remove the mutable opts mutation.

In `@packages/3-mongo-target/2-mongo-adapter/src/lowering.ts`:
- Around line 469-666: structuralLowerStage duplicates most logic from
lowerStage; extract a shared helper (e.g., buildLowerStage) that accepts
strategy functions for lowering components (filter, pipeline, exprRecord,
agg/window helpers as needed) and reuse it from both structuralLowerStage and
lowerStage by passing structuralLowerFilter/structuralLowerPipeline (sync) or
their async variants; update structuralLowerStage to call the helper to remove
the ~200-line duplication while preserving current behavior and signatures of
structuralLowerStage and lowerStage.

In `@packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts`:
- Around line 101-105: resolveDraftSlot is missing an immediate abort check for
consistency with resolveValue and resolveDraftDoc; add a call to
checkAborted(ctx, 'encode') at the start of the resolveDraftSlot function so it
fails fast if the context is aborted. Locate the async function
resolveDraftSlot(value, codecs, ctx) and insert the checkAborted(ctx, 'encode')
guard as the first statement to mirror the behavior in resolveValue and
resolveDraftDoc.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: b6ff1927-584b-47f7-aa37-0f009d539d03

📥 Commits

Reviewing files that changed from the base of the PR and between 3380721 and 8bb380d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • packages/2-mongo-family/6-transport/mongo-lowering/src/adapter-types.ts
  • packages/2-mongo-family/6-transport/mongo-lowering/src/exports/index.ts
  • packages/2-mongo-family/7-runtime/package.json
  • packages/2-mongo-family/7-runtime/src/exports/index.ts
  • packages/2-mongo-family/7-runtime/src/mongo-middleware.ts
  • packages/2-mongo-family/7-runtime/src/mongo-runtime.ts
  • packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts
  • packages/2-mongo-family/7-runtime/test/execute-param-mutator-wiring.test.ts
  • packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts
  • packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts
  • packages/2-mongo-family/7-runtime/test/param-ref-mutator.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/exports/runtime.ts
  • packages/3-mongo-target/2-mongo-adapter/src/lowering.ts
  • packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts
  • packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts
  • packages/3-mongo-target/2-mongo-adapter/test/structural-lower.test.ts

Comment thread packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts Outdated
Comment thread packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts Outdated
@wmadden-electric
Copy link
Copy Markdown
Contributor Author

Addressed the local review findings in two commits on this branch (d52340e85, 784d55b10):

  • F02 / F01 footgun (fail-loud): computeMongoContentHash now throws RUNTIME.CONTENT_HASH_REQUIRES_RESOLVED_COMMAND (O(1) prototype check) if called on a pre-resolve draft instead of a resolved wire command — so a middleware that calls ctx.contentHash during beforeExecute fails loudly with a clear message instead of a cryptic canonicalStringify error. Tests in content-hash-guard.test.ts.
  • F01 (honest contract, docs): JSDoc on MongoMiddleware.beforeExecute, MongoMiddlewareContext.contentHash, and MongoExecutionPlan.command now states that plan.command holds the unresolved draft during beforeExecute and that the supported param surface is the params mutator (entries() / replaceValue(s)), not structural reads of plan.command.
  • F03 (walk parity): verified flattenMongoParamRefs descends into exactly the same container shapes resolveParams resolves; documented the invariant and pinned it with flatten-resolve-parity.test.ts (insert/update/aggregate).
  • Docs (system-design follow-up): ADR 215 gains a Mongo lifecycle-parity subsection recording the two intentional SQL/Mongo asymmetries (public adapter phases; mutator in 7-runtime) and the stricter MongoLoweredDraft type; the @prisma-next/mongo-lowering README links to it.

Gates: @prisma-next/mongo-runtime 170 tests green, typecheck + lint:deps clean, no new bare casts.

Deliberately out of scope (follow-up TML-2752): the fully type-honest fix for F01 — a distinct pre-resolve plan type for beforeExecute so the draft is never typed as a resolved wire command — requires changing the framework RuntimeMiddleware<TPlan> SPI (single shared TPlan across all hooks), which is cross-family (SQL rides the same SPI). Tracked separately rather than folded into this Mongo PR.

@wmadden-electric wmadden-electric changed the title TML-2376: defer Mongo resolveValue past beforeExecute + wire param mutator TML-2376: Mongo middleware can rewrite params before encode May 31, 2026
wmadden added 8 commits May 31, 2026 13:44
…ms phases (TML-2376)

Introduce a two-phase lowering design in the Mongo adapter, mirroring
the SQL family's lowerToDraft / encodeDraftParams split:

- structuralLower(plan): synchronous; performs all structural transforms
  (collection, filter shape, pipeline stages, update shape) but leaves
  MongoParamRef nodes in place as unresolved leaves in the draft tree.
- resolveParams(draft, ctx): async; walks the draft, resolves every
  MongoParamRef leaf via the codec registry, and returns a frozen
  AnyMongoWireCommand.
- lower() is re-expressed as resolveParams(structuralLower(plan), ctx),
  preserving all existing observable behaviour.

Supporting changes:
- Add MongoLoweredDraft discriminated union to mongo-lowering/adapter-types.ts
  (and export it) so downstream phases can type the intermediate draft without
  depending on wire-command classes.
- Add structuralLowerFilter / structuralLowerStage / structuralLowerPipeline
  synchronous helpers in lowering.ts; existing lowerFilter / lowerStage /
  lowerPipeline now compose these with resolveDraftDoc.
- Add resolveDraftDoc / resolveDraftSlot to resolve-value.ts for recursive
  resolution of Record<string,unknown> draft structures.
- Expose structuralLower and resolveParams on MongoRuntimeAdapterInstance
  (exports/runtime.ts) so later dispatches can wire them into the runtime
  without modifying this package.
- New test file (test/structural-lower.test.ts) asserts MongoParamRef nodes
  survive in the draft for insertOne documents, field filters, $and filters,
  aggregate pipeline stages ($match, $addFields), updateOne, insertMany,
  rawInsertOne, rawAggregate, findOneAndUpdate, findOneAndDelete, and
  updateMany; also verifies lower() composition is unchanged.

Signed-off-by: Will Madden <madden@prisma.io>
… over MongoLoweredDraft (TML-2376)

Introduce the mutator surface that middleware authors will use to rewrite
MongoParamRef values before encode runs, mirroring the SQL family's
SqlParamRefMutator over SqlExecutionPlan.params.

New module packages/2-mongo-family/7-runtime/src/param-ref-mutator.ts:
- MongoParamRefHandle<TCodecId> — phantom-branded opaque token produced
  by entries(); callers pattern-match on entry.codecId to narrow to a
  typed replaceValue overload.
- MongoParamRefEntry / MongoParamRefEntryUnion — entry shape exposed to
  middleware, carrying ref, current effective value, and codecId.
- flattenMongoParamRefs(draft) — flat Generator walk over a
  MongoLoweredDraft yielding every MongoParamRef leaf (object values,
  array elements, filter predicates, update operators, pipeline stages).
  Raw-command variants yield zero entries.
- MongoParamRefMutator (public) — extends ParamRefMutator; entries(),
  replaceValue (typed overloads), replaceValues.
- MongoParamRefMutatorInternal — adds currentDraft() for the runtime.
- createMongoParamRefMutator(draft) — factory; overrides lazily allocated;
  currentDraft() returns original draft by reference when nothing was
  replaced (reference-identity fast path), otherwise rebuilds the tree
  with new MongoParamRef nodes carrying the replacement values.
- buildMutatedDraft / substituteSlot / substituteDoc / substituteUpdate —
  tree-walking reconstruction helpers, internal only.

Supporting changes:
- mongo-middleware.ts: type beforeExecute third arg as MongoParamRefMutator
  (was base ParamRefMutator marker); existing (plan) / (plan, ctx) shapes
  remain compatible — TypeScript permits fewer-param functions.
- runtime package.json: promote @prisma-next/mongo-value from devDependency
  to dependency (needed for instanceof MongoParamRef in source); update
  lockfile accordingly.
- exports/index.ts: expose mutator types and factories.
- test/mongo-middleware.test.ts, test/mongo-runtime-abort.test.ts: add
  vi.fn() stubs for structuralLower and resolveParams on the mock
  MongoRuntimeAdapterInstance (required after D1 extended MongoAdapter).
- test/param-ref-mutator.test.ts: 21 tests covering the flatten walk
  (object/array/filter/pipeline), replaceValues write-through, typed
  replaceValue overload (codecId-matched), preserves codecId/name on
  rebuilt refs, nested object and array element replacement, filter
  predicate and pipeline stage replacement, reference-identity fast path,
  and raw-command zero-entries no-op.

Signed-off-by: Will Madden <madden@prisma.io>
Defer value resolution past beforeExecute: structuralLower, mutator +
runBeforeExecuteChain, then resolveParams before the driver. Content hash
runs on the resolved wire command. Tests cover bulk middleware, fast path,
and hash discrimination after mutation.

Signed-off-by: Will Madden <madden@prisma.io>
Replace new bare `as` casts with blindCast or narrowing helpers so
lint:casts passes, and extend the mongo-runner stub with structuralLower
and resolveParams after the adapter interface split.

Signed-off-by: Will Madden <madden@prisma.io>
Use type predicates for draft update unions, replace bare casts with
blindCast in PR-touched paths, and extend param-ref-mutator tests to
restore 100% coverage on mongo-runtime.

Signed-off-by: Will Madden <madden@prisma.io>
Add structuralLower + resolveParams to the cross-family middleware mock
adapter so its create() instance satisfies MongoRuntimeAdapterInstance
after the adapter interface split.

Signed-off-by: Will Madden <madden@prisma.io>
…eware contract (TML-2376)

Guard computeMongoContentHash when plan.command is still a structural draft,
document the beforeExecute vs post-resolve plan lifecycle, and pin flatten/resolve
walk parity with tests.

Signed-off-by: Will Madden <madden@prisma.io>
…parity (TML-2376)

Document intentional Mongo vs SQL placement for the two-phase lowering
split and param mutator, and that Mongo execute now mirrors ADR 215's
pre-resolve middleware lifecycle.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden force-pushed the tml-2376-mongo-middleware-param-mutator-runtime-wiring-defer branch from 784d55b to b00339c Compare May 31, 2026 11:44
…(TML-2376)

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden merged commit 2e120e5 into main May 31, 2026
19 of 20 checks passed
@wmadden wmadden deleted the tml-2376-mongo-middleware-param-mutator-runtime-wiring-defer branch May 31, 2026 11:50
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