Mongo ORM: extendable Collection class + collections registry (ADR 175)#936
Mongo ORM: extendable Collection class + collections registry (ADR 175)#936ankur-arch wants to merge 9 commits into
Conversation
The Mongo ORM's chaining implementation was locked behind the
MongoCollection interface and createMongoCollection factory, so users
could not do what the SQL ORM supports:
class UserRepository extends Collection<Contract, 'User'> {
byEmail(email: string) { return this.where({ email }); }
}
The implementation class is now the exported, extendable `Collection`
(mirroring @prisma-next/sql-orm-client). Chaining methods clone through
this.constructor so a chain started from a subclass stays that subclass.
mongoOrm() and the mongo() facade accept a `collections` registry keyed
by model name; registered roots surface the subclass (typed via
InstanceType), unregistered roots keep the base collection.
The shared-interface extraction across families stays future work per
ADR 175's spike-then-extract plan; the ADR carries a status note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a subclassable Mongo ORM ChangesCustom Collection subclassing support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant mongo
participant mongoOrm
participant Collection
participant MongoQueryExecutor
mongo->>mongoOrm: pass contract, executor, and collections
mongoOrm->>Collection: instantiate registered subclass
Collection->>MongoQueryExecutor: execute chained query
MongoQueryExecutor-->>Collection: return query results
mongoOrm-->>mongo: expose custom collection roots
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@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/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@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: |
size-limit report 📦
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/3-extensions/mongo/src/runtime/mongo.ts (1)
34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the repeated
Collectionsconstraint into a shared alias.The clause
Collections extends Partial<Record<string, AnyMongoCollectionClass>> = Record<never, never>is duplicated 7 times in this file. A shared type alias would reduce drift risk if the constraint ever changes.♻️ Proposed refactor
+export type MongoCollectionsMap = Partial<Record<string, AnyMongoCollectionClass>>; + export interface MongoClient< TContract extends MongoContractWithTypeMaps<MongoContract, AnyMongoTypeMaps>, - Collections extends Partial<Record<string, AnyMongoCollectionClass>> = Record<never, never>, + Collections extends MongoCollectionsMap = Record<never, never>, > {(and apply the same substitution at the other six occurrences)
Also applies to: 67-67, 83-83, 99-99, 127-127, 131-131, 137-137
🤖 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-extensions/mongo/src/runtime/mongo.ts` at line 34, The `Collections` generic constraint is repeated across several declarations in `mongo.ts`, so extract `Collections extends Partial<Record<string, AnyMongoCollectionClass>> = Record<never, never>` into a shared type alias and replace all seven occurrences with that alias. Update the affected generic signatures in the relevant Mongo types/functions so the constraint is defined once and reused consistently.
🤖 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 `@docs/architecture` docs/adrs/ADR 175 - Shared ORM Collection interface.md:
- Line 5: The Mongo ORM example text in the ADR is stale and conflicts with the
updated status note; refresh the example heading and surrounding wording in the
ADR section that references the Mongo ORM target so it reflects that custom
collections are already implemented. Use the nearby Mongo example and the
shared-interface “spike then extract” language as anchors, and make sure the
wording is internally consistent with the `mongoOrm({ collections })`, `mongo({
collections })`, and extendable `Collection` behavior now described.
In `@packages/2-mongo-family/5-query-builders/orm/src/collection.ts`:
- Line 301: The include refinement in Collection should not use a bare
production cast; replace the existing `as unknown as Collection<...>` in the
include-cloning path with `blindCast<...>('include() refines the TIncludes
phantom type after cloning with the added include expression')` so the intent is
explicit and the no-bare-casts rule stays enforceable.
- Around line 830-839: The fluent query methods on Collection are widening
custom subclasses back to MongoCollection, which drops subclass-specific methods
after chaining. Update the return typing for the fluent builders in Collection
so methods like where, select, take, skip, orderBy, and include preserve the
current subtype via this or a self-type, and use `#clone` as the place to ensure
subclass instances are returned. Add a d.ts test covering a custom repository
subclass (for example UserRepository with a byName method) chained through
multiple query steps to verify the subclass type is retained.
In `@packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts`:
- Line 77: The final return in mongo-orm.ts still uses a bare type assertion;
replace the `client as MongoOrmClient<TContract, Collections>` cast with the
existing `blindCast` helper to keep production code consistent. Update the
return in the `createMongoOrm` flow so the `MongoOrmClient` value is wrapped via
`blindCast` instead of using `as`, and keep the already imported `blindCast`
symbol as the location anchor.
---
Nitpick comments:
In `@packages/3-extensions/mongo/src/runtime/mongo.ts`:
- Line 34: The `Collections` generic constraint is repeated across several
declarations in `mongo.ts`, so extract `Collections extends
Partial<Record<string, AnyMongoCollectionClass>> = Record<never, never>` into a
shared type alias and replace all seven occurrences with that alias. Update the
affected generic signatures in the relevant Mongo types/functions so the
constraint is defined once and reused consistently.
🪄 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: 6ec48a0c-9547-414e-99dc-c670fd24bffd
📒 Files selected for processing (10)
docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.mdpackages/2-mongo-family/5-query-builders/orm/src/collection.tspackages/2-mongo-family/5-query-builders/orm/src/exports/index.tspackages/2-mongo-family/5-query-builders/orm/src/mongo-orm.tspackages/2-mongo-family/5-query-builders/orm/test/custom-collection.test-d.tspackages/2-mongo-family/5-query-builders/orm/test/custom-collection.test.tspackages/3-extensions/mongo/src/exports/runtime.tspackages/3-extensions/mongo/src/runtime/mongo.tspackages/3-extensions/mongo/test/mongo.e2e.test.tspackages/3-extensions/mongo/test/mongo.test.ts
- where/select/orderBy/take/skip return `this` on both the MongoCollection interface and the Collection class, so custom -> base -> custom chains (repo.byName(x).take(1).newestFirst()) typecheck; include()/variant() re-parameterize generics and deliberately widen to the base type. - AnyMongoCollectionClass now requires the Collection type-level brand (string-key brand, mirroring ENUM_TYPE_HANDLE_BRAND), so only real Collection subclasses can be registered. - Custom construction is centralized in instantiateCustomCollection with an instanceof guard that throws a clear error for non-Collection classes. - Remaining production `as unknown as` casts replaced with blindCast + reason. - ADR 175 example no longer claims the Mongo side is unimplemented and shows the shipped object-form where/orderBy API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com>
…ngo collections surface Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com>
|
Ponytail review (lazy-senior-dev ladder over the final diff):
→ skipped: callback where-DSL on Mongo, subclass typing through |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com>
…able-collection Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> # Conflicts: # skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md
…validation Address review findings on the custom-collection registry. The runtime guard used `instance instanceof Collection`, which compares against this module copy's class identity. A subclass built against a duplicated copy of @prisma-next/mongo-orm in one bundle was rejected at boot — the same cross-copy failure `isPgPool`/`isPgClient` fixed for pg bindings. MONGO_ORM_COLLECTION_BRAND is now a real static marker on `Collection`, inherited by every subclass constructor, and the new `isMongoCollectionClass` guard reads it structurally. Registry validation now matches the SQL ORM's `createCollectionRegistry`: entries are checked before any of them is constructed (previously an arbitrary constructor ran with base-shaped args before the guard could reject it), a key that names no model throws instead of being silently dropped, and passing an instance where a class was expected reports that rather than a raw TypeError. Also drops the redundant `as UserRepository` / `as TaskRepository` casts in the runtime tests — the registry already infers the subclass, and the casts were masking what would be a typing regression. Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com>
Refresh pass — conflicts resolved, two review findings fixedMerge conflictResolved in Review findings fixed1. The runtime guard reintroduced the cross-copy The irony was that the brand comment already picked a string key over a unique symbol specifically to survive 2. Registry validation was strictly weaker than the SQL ORM it mirrors. Now matched against
3. Dropped the redundant Tests grew 236 → 240, covering: unknown-key rejection, instance-not-class rejection, no-construction-before-validation, and cross-copy recognition where Checks run
The previously red E2E Tests check passes locally after the Two flakes seen locally, both unrelated to this diff (it touches no SQL or LSP code):
Remaining risks / decisions for human review
|
There was a problem hiding this comment.
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/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts`:
- Around line 22-30: Update AnyMongoCollectionClass and MongoOrmOptions so
collection classes preserve their model type and require the exact (contract,
modelName, executor) constructor ABI used by mongoOrm(). Key the Collections
registry by each model name so a collection for one model cannot register under
another. Add negative type tests covering an extra required constructor argument
and mismatched model registration.
In `@packages/2-mongo-family/5-query-builders/orm/test/custom-collection.test.ts`:
- Around line 159-170: Update isMongoCollectionClass and its related test to
reject brand-only constructors that lack the Collection query protocol. Replace
ForeignCopyCollection with a realistic duplicated-package collection and
exercise registration plus .where()/.all() execution, while preserving
acceptance of valid UserRepository subclasses and rejection of unrelated
classes.
🪄 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: 8369cd64-530c-442d-87c6-725b09c9131b
📒 Files selected for processing (4)
packages/2-mongo-family/5-query-builders/orm/src/collection.tspackages/2-mongo-family/5-query-builders/orm/src/exports/index.tspackages/2-mongo-family/5-query-builders/orm/src/mongo-orm.tspackages/2-mongo-family/5-query-builders/orm/test/custom-collection.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts
- packages/2-mongo-family/5-query-builders/orm/src/collection.ts
| export type AnyMongoCollectionClass = new ( | ||
| ...args: never[] | ||
| ) => { | ||
| readonly [MONGO_ORM_COLLECTION_BRAND]: true; | ||
| }; | ||
|
|
||
| export interface MongoOrmOptions< | ||
| TContract extends MongoContract, | ||
| Collections extends Partial<Record<string, AnyMongoCollectionClass>> = Record<never, never>, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts --items all
rg -n -C3 'AnyMongoCollectionClass|CustomCollectionForModel|instantiateCustomCollection|new \(\.\.\.args: never\[\]' \
packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts
rg -n -C3 'class .*Repository|collections:|expect.*Assignable|expect.*Not' \
packages/2-mongo-family/5-query-builders/orm/test/custom-collection.test-d.ts \
packages/2-mongo-family/5-query-builders/orm/test/custom-collection.test.tsRepository: prisma/prisma-next
Length of output: 19094
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline packages/2-mongo-family/5-query-builders/orm/src/collection.ts --items all
sed -n '1,220p' packages/2-mongo-family/5-query-builders/orm/src/collection.ts | cat -n
printf '\n--- mongo-orm.ts ---\n'
sed -n '1,220p' packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts | cat -n
printf '\n--- custom-collection.test-d.ts ---\n'
sed -n '1,220p' packages/2-mongo-family/5-query-builders/orm/test/custom-collection.test-d.ts | cat -nRepository: prisma/prisma-next
Length of output: 23190
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path('packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts')
text = p.read_text()
for needle in [
'type CustomCollectionForModel',
'function instantiateCustomCollection',
'validateCollectionRegistry(',
'new (...args: never[])',
]:
print(f'=== {needle} ===')
idx = text.find(needle)
if idx != -1:
start = max(0, text.rfind('\n', 0, idx-120))
end = min(len(text), text.find('\n\n', idx+1))
print(text[start:end])
print()
else:
print('not found')
PYRepository: prisma/prisma-next
Length of output: 1782
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'collection.ts outline\n'
ast-grep outline packages/2-mongo-family/5-query-builders/orm/src/collection.ts --items all
printf '\ncollection.ts\n'
sed -n '1,240p' packages/2-mongo-family/5-query-builders/orm/src/collection.ts | cat -n
printf '\nmongo-orm.ts\n'
sed -n '1,220p' packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts | cat -n
printf '\ncustom-collection.test-d.ts\n'
sed -n '1,220p' packages/2-mongo-family/5-query-builders/orm/test/custom-collection.test-d.ts | cat -nRepository: prisma/prisma-next
Length of output: 24190
🏁 Script executed:
#!/bin/bash
set -euo pipefail
log=/tmp/coderabbit-shell-logs/shell-output-jqQ52x
sed -n '1,220p' "$log" | cat -nRepository: prisma/prisma-next
Length of output: 19119
Encode the registry constructor and model types.
AnyMongoCollectionClass still erases the collection model and constructor ABI, so a Collection<..., 'User'> can be registered under Task even though mongoOrm() always instantiates with (contract, modelName, executor). Tighten the registry to key subclasses by model and require the exact 3-arg constructor, then add negative type tests for an extra required ctor arg and a mismatched model registration.
🤖 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/5-query-builders/orm/src/mongo-orm.ts` around lines
22 - 30, Update AnyMongoCollectionClass and MongoOrmOptions so collection
classes preserve their model type and require the exact (contract, modelName,
executor) constructor ABI used by mongoOrm(). Key the Collections registry by
each model name so a collection for one model cannot register under another. Add
negative type tests covering an extra required constructor argument and
mismatched model registration.
| it('recognises a subclass from a duplicated package copy, where instanceof fails', () => { | ||
| class ForeignCopyCollection { | ||
| static readonly [MONGO_ORM_COLLECTION_BRAND] = true; | ||
| readonly modelName: string; | ||
| constructor(_contract: Contract, modelName: string, _executor: MongoQueryExecutor) { | ||
| this.modelName = modelName; | ||
| } | ||
| } | ||
| expect(ForeignCopyCollection.prototype instanceof Collection).toBe(false); | ||
| expect(isMongoCollectionClass(ForeignCopyCollection)).toBe(true); | ||
| expect(isMongoCollectionClass(UserRepository)).toBe(true); | ||
| expect(isMongoCollectionClass(class Unrelated {})).toBe(false); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Do not treat a brand-only class as a usable foreign Collection.
ForeignCopyCollection has no query API, yet passes isMongoCollectionClass. Since mongoOrm performs no later shape check, registering it produces a root whose .where()/.all() calls fail at runtime. Test a real duplicated-package collection (including execution), and reject brand-only constructors or validate the required protocol.
🤖 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/5-query-builders/orm/test/custom-collection.test.ts`
around lines 159 - 170, Update isMongoCollectionClass and its related test to
reject brand-only constructors that lack the Collection query protocol. Replace
ForeignCopyCollection with a realistic duplicated-package collection and
exercise registration plus .where()/.all() execution, while preserving
acceptance of valid UserRepository subclasses and rejection of unrelated
classes.
main has bumped to 0.15.0, so check:upgrade-coverage now requires per-PR declarations in 0.15-to-0.16/instructions.md. The entry was recorded against the already-shipped 0.14-to-0.15 cycle. Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com>
…able-collection Signed-off-by: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> # Conflicts: # skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md
What this ships
You can now write custom repository classes for the Mongo ORM, exactly like the SQL ORM (ADR 175):
Before this PR,
MongoCollectionwas interface-only —extends Collectionwas impossible on Mongo. This was reported on Discord ([NEXT], 2026-07-08); follow-up to #934.The changes
Collectionis exported and extendable from@prisma-next/mongo-orm(and@prisma-next/mongo/runtime). It is the same chaining implementation that already backed the interface — no behavior change.mongoOrm({ collections })andmongo({ collections })register subclasses by model name. Registered roots are typed as your subclass; everything else keeps the base type. Existing call sites compile unchanged.where,select,orderBy,take, andskipreturnthis, sousers.byEmail(x).take(1).newestFirst()typechecks.include()andvariant()change the collection's generic parameters, so they intentionally widen to the base type (documented on the class).Collectionsubclasses — enforced at the type level (a brand on the class) and at runtime (a clear error if a non-Collectionclass is registered from untyped code).Review feedback addressed (CodeRabbit + ponytail pass)
as unknown ascasts in the touched files replaced withblindCast+ reason.AnyMongoCollectionClasstightened from=> objectto the branded instance shape.instanceofguard and clear error message.#cloneWithVariantnow delegates to#clone. Skipped: the SQL-style callback where-DSL on Mongo and subclass typing throughinclude()/variant()— add when the cross-family where-DSL generalizes (ADR 175's extraction step).Testing
mongo-orm(registry wiring, plan parity with the base collection, subclass preservation, brand rejection, runtime guard).mongo({ collections })forwarding (unit) and a full round-trip against a real in-memory replica set (e2e).pnpm test:packagessweep (926/928 files; the 2 failures are machine-local flakes that reproduce on cleanmainand pass in isolation), mongo + emit integration suites,mongo-demoexample suite,pnpm lint:deps, typecheck/lint on both packages.🤖 Generated with Claude Code
Summary by CodeRabbit
collectionsregistry to the Mongo ORM (mongoOrm) andmongofacade for customCollectionsubclasses per model.CollectionplusMONGO_ORM_COLLECTION_BRANDandisMongoCollectionClassfor reliable custom-collection detection.