Conversation
…idation Captures the design agreed for type-safe dot-path validation on `FieldAccessor`s callable form: nested shape derivation from the contract, constrained callable signature with ArkType-style completions, reduced op surface on non-leaf paths, and additive-vs- replacement stage threading of the nested shape through the pipeline builder. Single-milestone execution plan with test-first sequencing and acceptance-criterion coverage mapping.
… FieldAccessor (TML-2281)
Close the known gap in the Mongo query-builder's callable field accessor:
`f("address.city")` is now compile-time validated against the contract's
model + value-object structure, returns an `Expression` carrying the
resolved leaf's codec, and surfaces IDE autocomplete over the valid
path union.
Core type machinery lives in `src/resolve-path.ts`:
- `NestedDocShape` / `ObjectField<N>` mark value-object paths and carry
sub-shapes alongside the flat `DocShape`.
- `ModelNestedShape` derives the initial nested tree from the contract.
- `ResolvePath<N, Path>` resolves a dot-path to a `DocField`.
- `ValidPaths<N>` produces the union of valid paths, short-circuiting to
`never` for open-ended index-signature shapes so the callable form is
automatically disabled downstream of replacement stages.
`Expression<F>` splits into `LeafExpression<F>` (full operator surface)
and `ObjectExpression<N>` (reduced `set`/`unset`/`exists`/`eq(null)`/
`ne(null)` surface for whole value objects) via a conditional type. The
runtime proxy is unchanged.
`FieldAccessor<S, N>` gains a second generic; `PipelineChain` threads
`N` as its fifth generic. Additive stages (match/addFields/sort/limit/
skip/sample/lookup/unwind/unionWith/densify/fill/redact/search/
vectorSearch) preserve `N`; replacement stages (project/group/
replaceRoot/count/sortByCount/bucket*/geoNear/facet/graphLookup/
setWindowFields/searchMeta/pipe) reset it to `Record<string, never>`.
`CollectionHandle` / `FilteredCollection` seed `N` from
`ModelNestedShape<TContract, ModelName>`.
Test fixtures extended with `Customer` + `Address`/`GeoPoint`/`Stats`
value objects (2-level nesting with a nullable intermediate). Adds
`resolve-path.test-d.ts` (18 tests) and `field-accessor.test-d.ts`
(22 tests) covering happy-path resolution, negative cases, non-leaf
reduced operator surface, and pipeline threading. Adds a runtime
non-regression assertion in `builder.test.ts` that the callable
dot-path still emits the same `MongoFieldFilter` node.
Docs: package README loses the "does not currently validate" caveat,
ADR 180's implementation note is updated to "Implemented in TML-2281",
and the parent project spec status table references this work.
…keyof stay concrete
The previous `FieldsToNested<TContract, Fields>` helper took the
`fields` record as a generic parameter and mapped over it internally.
When TypeScript instantiated that helper with
`TContract["models"]["X"]["fields"]` — an intersection of
`Record<string, ContractField>` (from the `MongoContract` base) and the
literal per-field record — the `keyof` inside the helper collapsed to
`string`, and the result hover degraded to `{ readonly [x: string]: any }`.
The value-side assertions in `resolve-path.test-d.ts` still returned
the right leaf types via indexed access under `tsc`, but the IDE hover
and intellisense surface were effectively broken (and any future test
that relied on `keyof CustomerShape` would tautologically pass against
`any`).
Fix by inlining the mapped iteration at the `ModelNestedShape` entry
point and at the VO recursion point (`VONestedShape`), matching the
homomorphic pattern already used by `ModelToDocShape`. `TranslateField`
is kept as a per-field helper with a single non-record generic
parameter, so it does not re-introduce the intersection collapse.
Adds a guard assertion to `resolve-path.test-d.ts` that `keyof
CustomerShape` is exactly the literal field-name union and that
`string extends keyof CustomerShape` is `false`, so any regression into
the open-index-signature shape fails loudly at the top of the suite
instead of silently making every downstream assertion vacuous.
…aths (TML-2281 review F12)
Strict dot-path validation (shipped in `db88f733c`) breaks a legitimate
use case: data-backfill migrations that write to fields not yet present
in the pre-migration contract. The retail-store `backfill-product-status`
migration intentionally used the callable form `f("status")` because
`status` is the field being backfilled — strict validation rejects that
path as "not assignable to parameter of type `never`" even though the
runtime is fine. Until now there was no sanctioned way out.
Add `f.raw<F extends DocField = DocField>(path: string): LeafExpression<F>`
on `FieldAccessor`:
- Accepts any string; no `ValidPaths<N>` constraint, no IDE autocomplete.
- Returns the full leaf operator surface (`set`, `exists`, `inc`, `push`,
`unset`, …). Default `F = DocField`; callers can narrow the return
via the explicit generic: `f.raw<StringField>("status").set("active")`.
- Independent of `N`, so it remains usable in the "callable disabled"
state downstream of replacement stages.
- Runtime uses the same `buildExpression(path)` helper as the strict
callable, so emitted filter/update nodes are byte-identical.
Runtime non-regression: `MongoExistsExpr.notExists("status")` produced
via `f.raw("status").exists(false)` is `.toEqual`-equivalent to the
low-level constructor, asserted in `builder.test.ts`.
Documents the escape hatch in the package README, ADR 180 implementation
note, and the spec/plan (resolving a new Open Item carried forward from
the F12 review finding).
…(TML-2281 review F12/F13)
Two linked review findings on TML-2281:
F12 — The `backfill-product-status` migration used the callable form
`f("status")` to write a field not yet present in the Product contract.
Strict validation shipped with TML-2281 rejected the path. Switch to the
new `f.raw("status")` escape hatch (introduced in the preceding commit)
so the migration compiles and the emitted runtime nodes are unchanged.
F13 — `examples/retail-store/tsconfig.json` excluded `migrations/**`
from `include`, so `pnpm --filter retail-store typecheck` silently
passed even while `migration.ts` was broken. Add `migrations/**/*.ts`
to the `include` list so the migration directory participates in the
typecheck going forward; this is the CI signal that surfaces regressions
like F12 at authoring time rather than at run time.
Combined with the `family-mongo` operation-union fix, `pnpm --filter
retail-store typecheck` now passes with migrations included.
📝 WalkthroughWalkthroughAdds compile-time, type-safe dot-path field access by threading a nested document-shape generic through pipeline chains, splitting expression types into leaf vs object surfaces, introducing path-resolution utilities and a Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/middleware-telemetry
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/runtime-executor
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/2-mongo-family/5-query-builders/query-builder/README.md`:
- Line 97: The ADR link in README.md currently uses a relative path that climbs
one directory too far; update the link target for "ADR 180 — Dot-path field
accessor" so it points into the repository's docs tree (e.g., adjust the
../../../../../docs/... path to the correct relative path that reaches
docs/architecture docs/adrs/ADR 180 — Dot-path field accessor.md from this
README), ensuring the rendered link resolves to the repo's docs/ directory.
In
`@packages/2-mongo-family/5-query-builders/query-builder/src/field-accessor.ts`:
- Around line 183-199: The escape-hatch method named raw on the FieldAccessor
type is shadowing legitimate top-level fields called "raw"; rename the
escape-hatch to a non-property name (e.g., rawPath or rawField) and update the
FieldAccessor signature (the method raw<F extends DocField = DocField>(path:
string): LeafExpression<F>) to the new name, preserving the same generic and
return type, then update all call sites and any other duplicate declarations of
the escape hatch (the other occurrence similar to the one at the end of the
file) so callers use the new identifier and no top-level field named "raw" is
overridden; keep ValidPaths, ResolvePath, LeafExpression and DocField semantics
unchanged.
🪄 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: b723d828-878a-47d8-8579-edeade6b0c4d
⛔ Files ignored due to path filters (3)
projects/mongo-pipeline-builder/plans/callable-field-accessor-path-validation-plan.mdis excluded by!projects/**projects/mongo-pipeline-builder/spec.mdis excluded by!projects/**projects/mongo-pipeline-builder/specs/callable-field-accessor-path-validation.spec.mdis excluded by!projects/**
📒 Files selected for processing (14)
docs/architecture docs/adrs/ADR 180 - Dot-path field accessor.mdexamples/retail-store/migrations/20260416_backfill-product-status/migration.tsexamples/retail-store/tsconfig.jsonpackages/2-mongo-family/5-query-builders/query-builder/README.mdpackages/2-mongo-family/5-query-builders/query-builder/src/builder.tspackages/2-mongo-family/5-query-builders/query-builder/src/exports/index.tspackages/2-mongo-family/5-query-builders/query-builder/src/field-accessor.tspackages/2-mongo-family/5-query-builders/query-builder/src/resolve-path.tspackages/2-mongo-family/5-query-builders/query-builder/src/state-classes.tspackages/2-mongo-family/5-query-builders/query-builder/test/builder.test.tspackages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.tspackages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-contract.tspackages/2-mongo-family/5-query-builders/query-builder/test/resolve-path.test-d.tspackages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts
The README lives 4 directories deep under the repo root, so its link to ADR 180 needs four `..`, not five. The extra `..` was causing the link to point outside the repository tree and miss `docs/` entirely. Addresses PR #361 review comment from CodeRabbit (discussion_r3118221548).
…dowing real fields
The escape hatch was originally named `raw`, which silently shadowed any
legitimate top-level `raw` field on a user model: the intersection of
the mapped-type property form and the extra method both resolve at
`f.raw`, and the runtime Proxy intercepted `raw` before falling through
to `buildExpression`, so property access for a real `raw` field became
unreachable. The callable fallback `f("raw")` does not cover this case
either — the callable is disabled downstream of replacement stages, so
such a field would be permanently inaccessible once a `project` / `group`
/ `replaceRoot` was introduced.
Rename the escape hatch to `rawPath` so it lives off the property
namespace. The name also reads as exactly what it is: a raw, unvalidated
path string. All call sites in the query-builder tests, the retail-store
`backfill-product-status` migration, README, ADR 180, spec, and plan
are updated. A regression type test in field-accessor.test-d.ts pins the
non-shadowing guarantee for a model with a top-level `raw` field.
Addresses PR #361 review comment from CodeRabbit
(discussion_r3118221588, TML-2281 review round 2).
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/2-mongo-family/5-query-builders/query-builder/src/field-accessor.ts (1)
206-245: Consider documenting the cast safety invariant more explicitly.The
as unknown as Expression<F>cast at line 244 violates the coding guideline against blind casts in production code. However, the pattern is technically safe here because:
ObjectExpressionis a strict subset ofLeafExpression(reduced operator surface)- The runtime object implements the full
LeafExpressionsurface- Compile-time gating via
Expression<F>prevents misuseThe comment at lines 207-210 explains what happens but not why the cast is safe. Consider adding an explicit safety note, e.g.:
// SAFETY: ObjectExpression<N> ⊂ LeafExpression<F> structurally — all // ObjectExpression methods (set/unset/exists/eq/ne) exist on this object. // The conditional Expression<F> type restricts access at compile time.This would help future maintainers understand the invariant that keeps this cast sound. As per coding guidelines: "No blind casts in production code:
as unknown as Xis forbidden outside tests."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/2-mongo-family/5-query-builders/query-builder/src/field-accessor.ts` around lines 206 - 245, Add an explicit SAFETY comment above the final cast in buildExpression explaining why the as unknown as Expression<F> is sound: state that ObjectExpression is a strict subset of LeafExpression, that the returned object implements the full LeafExpression runtime surface (list the implemented methods like eq, ne, set, unset, exists, push, etc.), and that the generic Expression<F> type provides compile-time gating to restrict usage; reference the types Expression<F>, ObjectExpression, LeafExpression and the buildExpression function so future maintainers can see the invariant that justifies the cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@packages/2-mongo-family/5-query-builders/query-builder/src/field-accessor.ts`:
- Around line 206-245: Add an explicit SAFETY comment above the final cast in
buildExpression explaining why the as unknown as Expression<F> is sound: state
that ObjectExpression is a strict subset of LeafExpression, that the returned
object implements the full LeafExpression runtime surface (list the implemented
methods like eq, ne, set, unset, exists, push, etc.), and that the generic
Expression<F> type provides compile-time gating to restrict usage; reference the
types Expression<F>, ObjectExpression, LeafExpression and the buildExpression
function so future maintainers can see the invariant that justifies the cast.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: a0883eaf-15f9-46f2-b6f7-b63ae75bb5ce
⛔ Files ignored due to path filters (2)
projects/mongo-pipeline-builder/plans/callable-field-accessor-path-validation-plan.mdis excluded by!projects/**projects/mongo-pipeline-builder/specs/callable-field-accessor-path-validation.spec.mdis excluded by!projects/**
📒 Files selected for processing (6)
docs/architecture docs/adrs/ADR 180 - Dot-path field accessor.mdexamples/retail-store/migrations/20260416_backfill-product-status/migration.tspackages/2-mongo-family/5-query-builders/query-builder/README.mdpackages/2-mongo-family/5-query-builders/query-builder/src/field-accessor.tspackages/2-mongo-family/5-query-builders/query-builder/test/builder.test.tspackages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts
✅ Files skipped from review due to trivial changes (1)
- examples/retail-store/migrations/20260416_backfill-product-status/migration.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/architecture docs/adrs/ADR 180 - Dot-path field accessor.md
- packages/2-mongo-family/5-query-builders/query-builder/README.md
The README lives 4 directories deep under the repo root, so its link to ADR 180 needs four `..`, not five. The extra `..` was causing the link to point outside the repository tree and miss `docs/` entirely. Addresses PR #361 review comment from CodeRabbit (discussion_r3118221548).
closes TML-2281
Intent
Close the framework gap where the query-builder's callable field accessor —
f("address.city")— accepted anystringand returned an untypedExpression<DocField>. This branch makes the callable form as type-safe as the property form: paths are constrained to the contract's valid dot-paths, invalid paths are compile-time errors with a useful diagnostic, and the returned expression carries the resolved leaf's codec so existing operator-surface semantics (and the future trait-gating hook per ADR 202) work correctly.A sanctioned escape hatch —
f.raw("path")— is provided for the one legitimate consumer that genuinely needs unvalidated paths: data-migration backfills writing to fields that are not yet part of the typed contract. The escape hatch is explicit, independent of the threadedNshape, and emits byte-identical runtime nodes to the strict callable.The change is intentionally type-level only — no new AST nodes, no wire changes, no runtime behavior drift.
Change map
Core implementation:
NestedDocShape,ObjectField<N>,ModelNestedShape,ResolvePath,ValidPaths,PathCompletions. Mapped iteration is inlined atModelNestedShape/VONestedShapeso the translation stays homomorphic over the sourcefieldsrecord (preserves literal keys through TypeScript's intersection-collapsing behavior).LeafExpression/ObjectExpressionsplit with a conditionalExpression<F>, new second genericNonFieldAccessor, thef.rawescape hatch, and a proxy factory that dispatchesrawandstagespecially.PipelineChaingains a fifth genericNthreaded through#withStagewith a per-stage "preserve vs reset" policy.N = ModelNestedShape<TContract, ModelName>atfrom(...)entry and carries it into every write-terminal callback.LeafExpression,ObjectExpression,ObjectField,NestedDocShape,ModelNestedShape,ResolvePath,ValidPaths,PathCompletions.Escape hatch (
f.raw):FieldAccessor<S, N>gainsraw<F extends DocField = DocField>(path: string): LeafExpression<F>.gethandler interceptsprop === 'raw'and returns(path: string) => buildExpression<DocField>(path)— the same helper the strict callable uses, so emitted nodes are byte-identical.Adjacent fixes:
MongoMigrationPlanOperation(DDL-only) toAnyMongoMigrationOperation(DDL +dataTransform). Pre-existing latent bug that was invisible untilmigrations/**/*.tsentered tsconfig scope."migrations/**/*.ts"toincludeso migration files are typechecked in CI.f('status')tof.raw('status'), demonstrating the migration-authoring pattern.Tests (evidence):
ModelNestedShape,ResolvePath,ValidPathsagainst theCustomerfixture. An anti-regression guard pinskeyof CustomerShapeto the literal field-name union and assertsstring extends keyof CustomerShapeisfalse, so any future regression into the open-index-signature shape fails loudly instead of silently making downstream assertions vacuous.ObjectExpression, negative paths, pipeline threading, and the fivef.rawscenarios.Customermodel +Address/GeoPoint/Statsvalue objects (two levels of nesting, one nullable intermediate).f('address.city').eq('NYC')emits the sameMongoFieldFilternode) and forf.raw(f.raw('status').exists(false)emitsMongoExistsExpr.notExists('status')byte-for-byte).GetU/GetFprobes carry the new fifth generic.The story
NestedDocShape = Record<string, DocField>and a newObjectField<N> extends DocFieldgive the type system a tree that mirrors the contract's model + value-object graph.ModelNestedShape<TContract, ModelName>walks a model'sfieldstable (scalar → leaf with concretecodecId/nullable;valueObject→ObjectFieldwhosefieldspayload is the recursively-resolved sub-shape;many: true→ leaf at the array boundary;union→ opaqueDocFieldleaf for now). The mapped iteration is inlined at the entry points so the mapped type stays homomorphic over the sourcefieldsrecord and literal keys survive TypeScript's intersection-collapsing machinery — without this, the translation silently degrades to{ readonly [x: string]: any }and every downstream type assertion becomes vacuous.ResolvePath<N, Path>pattern-matchesPathagainst${Head}.${Rest}, recurses into theObjectField<Sub>atHead, and terminates on a leafDocFieldornever.ValidPaths<N>is the distributed template-literal union of every valid (leaf or intermediate) path.string extends keyof N ? never : …guards the open-endedRecord<string, never>case so the callable form is automatically disabled whenNcarries no structural information.FieldAccessor<S, N>becomes a second-generic type. The callable overload is<P extends ValidPaths<N>>(path: P) => Expression<ResolvePath<N, P>>— paths the contract doesn't know about are compile-time errors, and the returned expression carries the resolved leaf's codec. Property access (f.status) is unchanged.Expression<F>becomes a conditional:F extends ObjectField<infer N>resolves to a newObjectExpression<N>interface exposing only the ops that make sense on a whole value object (set,unset,exists,eq(null),ne(null)). Everything else resolves toLeafExpression<F>(today's full operator surface). Runtime is unchanged — the proxy builds one uniform implementation that satisfies both type shapes.Nthrough the pipeline.PipelineChaingains a fifth type parameter. Stages that preserve document structure (match,sort,limit,skip,sample,addFields,lookup,unwind,redact,densify,fill,search,vectorSearch,unionWith) re-passNexplicitly; stages that rewrite the document (group,project,replaceRoot,count,sortByCount,bucket*,facet,geoNear,graphLookup,setWindowFields,searchMeta,pipe) omit the type argument and pick up theRecord<string, never>default, resettingNand disabling the callable form downstream.Nat the entry points.CollectionHandleandFilteredCollectionextendPipelineChain<…, ModelNestedShape<TContract, ModelName>>and pass that derived shape into everycreateFieldAccessorcall site (match,updateMany,updateOne,updateAll,upsertOne,findOneAndUpdate). The derived shape is computed once per model (not per stage), so the typecheck cost per stage is a cheap parameter carry.FieldAccessor<S, N>gainsraw<F extends DocField = DocField>(path: string): LeafExpression<F>— explicitly unvalidated (any string), full leaf operator surface, independent ofN(so still callable after replacement stages or under the default-disabled state), same runtime helper as the strict callable (so nodes are byte-identical). The retail-store backfill migration consumesf.raw('status')against a field that is not in the typedProductshape;examples/retail-store/tsconfig.jsonnow includesmigrations/**/*.tsso regressions in this layer surface at typecheck time. A pre-existing latent bug inMongoMigration's operation-union parameter (pinned to DDL-only ops) also surfaced once the migrations directory entered typecheck scope, and was fixed in the same wave.ObjectExpressionsurface, the additive-vs-replacement pipeline policy, and the fivef.rawscenarios (arbitrary path, full surface, paths outsideValidPaths<N>, availability under emptyN, explicit generic narrowing). Two runtime tests assert byte-identical filter-node emission for the strict callable and forf.raw, guarding against accidental runtime divergence whenExpression<F>was split.Behavior changes & evidence
Behavior change A — Callable-form dot-paths are now contract-validated
Before → After:
Why: Typos and stale paths after contract refactors used to surface only at runtime as zero-match updates or "field not found" oddities. Constraining paths at the call site turns them into IDE-surfaced errors and gives the returned expression a concrete codec (future trait-gating hook per ADR 202).
Implementation:
LeafExpression/ObjectExpression/ conditionalExpression,FieldAccessor<S, N>.Tests:
ValidPathsunion.@ts-expect-errorcoverage of bogus paths and over-traversal.Behavior change B — Non-leaf paths (
f("address")) expose a reduced operator surfaceNew capability:
Why: Whole-value-object operations (
set/unset/ presence checks) are the set of operators that actually make sense on a structured sub-document. Arithmetic, comparison, and array operators belong on the constituent leaves (f("address.city"),f("stats.visits")). The conditionalExpression<F>gives us type-level narrowing without changing the runtime shape.Implementation:
ObjectExpression<N>interface + conditionalExpression<F>.Tests:
Behavior change C — Replacement pipeline stages disable the callable form downstream
Before → After:
Why: After a
$group/$project/$replaceRoot/ etc., the source document's nested-path tree is no longer meaningful — the pipeline has produced a new shape whose paths live inShape, not in the original contract model. ResettingNkeeps the callable form sound with no additional work from the stage author: the default generic argument on#withStagemakes "reset" the behavior a replacement stage gets for free.Implementation:
#withStagewithNewN = Record<string, never>default; additive stages re-threadNexplicitly, replacement stages omitNand inherit the default.Tests:
Behavior change D —
f.raw(path)escape hatch for unvalidated pathsBefore → After:
Why: Data-migration backfills are a first-class use case that by definition write to fields the post-migration contract knows about but the pre-migration contract doesn't. The strict callable's "not assignable to parameter of type
never" error was correct for the common case but wrong for this one.f.rawis the sanctioned opt-out: explicit, named, searchable at call sites, and lints obvious in review ("why is this usingraw?" is easier to audit than "why is this cast withas never?"). Independence fromNmeans it remains usable after replacement stages resetN.Implementation:
FieldAccessor<S, N>type withraw<F>; proxygethandler dispatches'raw'to(path) => buildExpression<DocField>(path).Tests:
ValidPaths<N>, availability whenNis empty, explicit generic narrowing.MongoExistsExpr.notExists('status').pnpm --filter retail-store typecheck, which now includesmigrations/**/*.ts.Behavior change E — Runtime emission is provably unchanged
Why flagged:
Expression<F>was split into a conditional type with anas unknown as Expression<F>assertion inbuildExpression. It is easy for such a split to accidentally drop a method on the runtime object; the accessor would still typecheck but throw at runtime.Evidence:
MongoFieldFilter.eq("address.city", "NYC")byte-for-byte, andf.raw('status').exists(false)emitsMongoExistsExpr.notExists('status')byte-for-byte.Compatibility / migration / risk
FieldAccessorand fifth generic onPipelineChainboth have defaults (Record<string, never>). Existing code that instantiates these types without explicit arguments continues to compile; the callable form in those codepaths is(path: never) => …, which was already the design intent for "disabled" call sites. The ORM package has its ownfield-accessor.tsand does not consume@prisma-next/mongo-query-builder'sFieldAccessor, so there is no cross-package ripple there.status) that is not yet in the typedProductshape. It now usesf.raw('status')andexamples/retail-store/tsconfig.jsonincludesmigrations/**/*.tsin theincludelist so the migration directory participates inpnpm --filter retail-store typecheck. That tsconfig extension also surfaced a pre-existing latent bug inMongoMigration's operation-union parameter (pinned to DDL-only ops, rejectingdataTransform) — fixed in the same wave.FieldAccessor<S, N>reservesrawandstageas property names on the proxy, but the type does not subtract those keys from the mapped[K in keyof S]half. A contract field namedraworstagetypechecks as if both the Expression and the reserved surface were available, while the runtime proxy returns only the reserved surface (no Expression methods).stagepredates TML-2281;rawis new. Mitigation options range from a README note to subtracting the reserved keys from the mapped type — see Follow-ups.f.rawdispatch in the accessor proxy.MongoAggFieldRef.of(path)andMongoFieldFilter.*(path, …)nodes are unchanged.ValidPaths<N>is a single recursive distributive conditional per stage;PathCompletions = ValidPathsmeans only one recursive union is built. No depth cap ships; monitor-and-add-cap-if-CI-regresses is the stated plan, and the two-levelCustomerfixture shows no pathology.ObjectField.codecId = 'prisma/object@1'is a type-level sentinel, never consumed at runtime. If a future code path addscodecRegistry.get(field.codecId)over nested paths, we'd need to either register an object codec or suppress the lookup forObjectFields.Follow-ups / open questions
PathCompletionsprogressive form. The spec's AC calls for"address."prefix completions; the current implementation aliasesPathCompletions = ValidPaths. Either implement the ArkType-style progressive completion (and intersect it at the callable's parameter position) or narrow the spec and README and downgrade the AC.nullable = trueonto its leaves; not implemented. Decide whether to implement or to explicitly update the spec to "leaf nullability only".NavItem.children: NavItem) is only implicitly covered by the suite typecheck; add the fixture for an explicit test.f.raw/f.stageproperty-name collision. Document the reserved names in the accessor docstring and README, or subtract them from the mapped type so collisions become a compile-time error pointing at the offending field.f.rawafter replacement stages. The "independent ofN" guarantee is tested only on a standalone accessor, not in the real.group(...).match(f => f.raw(...))path. One short test would close the gap.Non-goals / intentionally out of scope
tags.0,items.$[elem]).addFields/project-computed fields re-enteringN(computed fields becoming callable-addressable in downstream stages).LeafExpression<F>per ADR 202.PathCompletions/ValidPaths.Summary by CodeRabbit
New Features
Documentation
Tests
Chores