Skip to content

Mongo ORM: extendable Collection class + collections registry (ADR 175)#936

Open
ankur-arch wants to merge 9 commits into
mainfrom
feat/mongo-orm-extendable-collection
Open

Mongo ORM: extendable Collection class + collections registry (ADR 175)#936
ankur-arch wants to merge 9 commits into
mainfrom
feat/mongo-orm-extendable-collection

Conversation

@ankur-arch

@ankur-arch ankur-arch commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What this ships

You can now write custom repository classes for the Mongo ORM, exactly like the SQL ORM (ADR 175):

import { Collection } from '@prisma-next/mongo-orm'; // also re-exported from @prisma-next/mongo/runtime

class UserRepository extends Collection<Contract, 'User'> {
  byEmail(email: string) { return this.where({ email }); }
  newestFirst()          { return this.orderBy({ _id: -1 }); }
}

const db = mongo({ contract, url, collections: { User: UserRepository } });

await db.orm.users.byEmail('alice@example.com').newestFirst().take(10).all();

Before this PR, MongoCollection was interface-only — extends Collection was impossible on Mongo. This was reported on Discord ([NEXT], 2026-07-08); follow-up to #934.

The changes

  • Collection is 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 }) and mongo({ collections }) register subclasses by model name. Registered roots are typed as your subclass; everything else keeps the base type. Existing call sites compile unchanged.
  • Chains keep your subclass, at runtime and in the types: where, select, orderBy, take, and skip return this, so users.byEmail(x).take(1).newestFirst() typechecks. include() and variant() change the collection's generic parameters, so they intentionally widen to the base type (documented on the class).
  • The registry only accepts real Collection subclasses — enforced at the type level (a brand on the class) and at runtime (a clear error if a non-Collection class is registered from untyped code).

Review feedback addressed (CodeRabbit + ponytail pass)

  • Fluent methods no longer widen to the base type mid-chain; a type test covers custom → base → custom chaining.
  • All production as unknown as casts in the touched files replaced with blindCast + reason.
  • AnyMongoCollectionClass tightened from => object to the branded instance shape.
  • Custom construction centralized in one helper with an instanceof guard and clear error message.
  • ADR 175 no longer claims the Mongo custom-collection example is "not yet implemented"; its example now shows the shipped API.
  • Minimalism pass: deleted the redundant constructor type alias; #cloneWithVariant now delegates to #clone. Skipped: the SQL-style callback where-DSL on Mongo and subclass typing through include()/variant() — add when the cross-family where-DSL generalizes (ADR 175's extraction step).

Testing

  • 12 unit tests + 6 type tests in mongo-orm (registry wiring, plan parity with the base collection, subclass preservation, brand rejection, runtime guard).
  • Facade tests: mongo({ collections }) forwarding (unit) and a full round-trip against a real in-memory replica set (e2e).
  • Full pnpm test:packages sweep (926/928 files; the 2 failures are machine-local flakes that reproduce on clean main and pass in isolation), mongo + emit integration suites, mongo-demo example suite, pnpm lint:deps, typecheck/lint on both packages.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a model-keyed collections registry to the Mongo ORM (mongoOrm) and mongo facade for custom Collection subclasses per model.
    • Exposed an extendable Collection plus MONGO_ORM_COLLECTION_BRAND and isMongoCollectionClass for reliable custom-collection detection.
  • Bug Fixes
    • Improved fluent chaining so registered subclass types (including domain helpers) are preserved consistently, and polymorphic behavior remains stable across chaining/variants.
  • Documentation
    • Updated the Mongo architecture/ADR with Mongo-specific pipeline notes and documented the custom-collection rollout.
    • Added an upgrade note for ADR 175.
  • Tests
    • Added typing, unit, and end-to-end coverage for registration, chaining, and error handling.

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>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a subclassable Mongo ORM Collection, maps registered custom collections onto ORM roots, forwards a collections registry through mongo(), and updates tests plus documentation for the shipped custom-collection flow.

Changes

Custom Collection subclassing support

Layer / File(s) Summary
Collection subclassing
packages/2-mongo-family/5-query-builders/orm/src/collection.ts, packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts
Exports Collection, updates fluent methods and cloning to preserve subclass identity, and re-exports the collection surface publicly.
mongoOrm collections registry
packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts
Adds a collections registry, custom-collection type mapping, runtime instantiation of registered subclasses, and validation that registered classes extend Collection.
mongo extension wiring
packages/3-extensions/mongo/src/runtime/mongo.ts, packages/3-extensions/mongo/src/exports/runtime.ts
Threads Collections through MongoClient and mongo() options, forwards collections into mongoOrm, and re-exports Mongo ORM collection types and value.
Behavior validation and release documentation
packages/2-mongo-family/5-query-builders/orm/test/*, packages/3-extensions/mongo/test/*, docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md, skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md, skills/extension-author/prisma-next-extension-upgrade/upgrades/0.15-to-0.16/instructions.md
Adds typing, runtime, facade, and e2e coverage for registration, chaining, validation, execution, and forwarding, plus ADR and upgrade documentation for the shipped API.

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
Loading

Suggested reviewers: wmadden-electric

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: an extendable Mongo Collection class and collections registry for ADR 175.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mongo-orm-extendable-collection

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@936

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/config-loader

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

@prisma-next/emitter

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

@prisma-next/language-server

npm i https://pkg.pr.new/@prisma-next/language-server@936

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 5f88ae5

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 160.62 KB (0%)
postgres / emit 144.06 KB (0%)
mongo / no-emit 99.49 KB (+0.2% 🔺)
mongo / emit 89.66 KB (+0.26% 🔺)
cf-worker / no-emit 186.8 KB (0%)
cf-worker / emit 168.17 KB (0%)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/3-extensions/mongo/src/runtime/mongo.ts (1)

34-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated Collections constraint 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

📥 Commits

Reviewing files that changed from the base of the PR and between f44cbcb and 0d16b5f.

📒 Files selected for processing (10)
  • docs/architecture docs/adrs/ADR 175 - Shared ORM Collection interface.md
  • packages/2-mongo-family/5-query-builders/orm/src/collection.ts
  • packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts
  • packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts
  • 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.ts
  • packages/3-extensions/mongo/src/exports/runtime.ts
  • packages/3-extensions/mongo/src/runtime/mongo.ts
  • packages/3-extensions/mongo/test/mongo.e2e.test.ts
  • packages/3-extensions/mongo/test/mongo.test.ts

Comment thread packages/2-mongo-family/5-query-builders/orm/src/collection.ts Outdated
Comment thread packages/2-mongo-family/5-query-builders/orm/src/collection.ts
Comment thread packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts Outdated
ankur-arch and others added 2 commits July 9, 2026 16:13
- 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>
@ankur-arch

Copy link
Copy Markdown
Contributor Author

Ponytail review (lazy-senior-dev ladder over the final diff):

  • Necessity — every addition maps to a reported user need or a review item; no speculative surface. The one new abstraction (instantiateCustomCollection) was reviewer-requested and is 20 lines.
  • Reuse — clone-through-this.constructor, the string-key brand, and blindCast all follow existing repo patterns (SQL Collection, ENUM_TYPE_HANDLE_BRAND, no-bare-casts).
  • Deletion over addition — the redundant MongoCollectionConstructor alias is gone, #cloneWithVariant now delegates to #clone, and the this-typed interface signatures are shorter than what they replaced. Repo-wide cast ratchet went down (lint:casts delta −7).
  • Deliberate shortcuts, markedinclude()/variant() widen the static type back to the base collection (documented on the class and interface); the SQL-style callback where-DSL is not ported.

→ skipped: callback where-DSL on Mongo, subclass typing through include()/variant(), cross-family interface extraction — add when ADR 175's where-DSL generalization lands.

ankur-arch and others added 4 commits July 9, 2026 17:06
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>
@ankur-arch

Copy link
Copy Markdown
Contributor Author

Refresh pass — conflicts resolved, two review findings fixed

Merge conflict

Resolved in skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md. Both sides had appended entries to the same list of upgrade declarations; the resolution keeps every entry from main (TML-2883 RLS, #962, #969, #976, #979, #915) and re-appends the ADR-175 Mongo entry at the end. No behavioural content was dropped from either side.

Review findings fixed

1. The runtime guard reintroduced the cross-copy instanceof bug class fixed in #969. instantiateCustomCollection compared instance instanceof Collection, which tests this module copy's class identity. A subclass built against a duplicated copy of @prisma-next/mongo-orm in one bundle (user depends on it directly at a different version than @prisma-next/mongo resolves, or a bundler duplicates it) would be rejected at boot with must extend the Collection class — exactly the failure shape isPgPool/isPgClient fixed for pg bindings.

The irony was that the brand comment already picked a string key over a unique symbol specifically to survive .d.mts dedup across package boundaries — but the brand was declare-only, so it couldn't back a runtime check. MONGO_ORM_COLLECTION_BRAND is now also a real static marker on Collection, inherited by every subclass constructor, and a new exported isMongoCollectionClass guard reads it structurally instead of using instanceof.

2. Registry validation was strictly weaker than the SQL ORM it mirrors. Now matched against sql-orm-client's createCollectionRegistry:

  • Unknown keys are no longer silently dropped. collections: { Uesr: UserRepository } (typo) previously compiled and silently gave you the base collection. It now throws No model found for custom collection 'Uesr'. Available models: ….
  • Validation happens before construction. The guard previously ran after new Ctor(contract, modelName, executor) had already invoked an arbitrary constructor with base-shaped args. All entries are now checked up front.
  • Instance-instead-of-class reports that explicitly rather than surfacing a raw TypeError: X is not a constructor.

3. Dropped the redundant as UserRepository / as TaskRepository casts in custom-collection.test.ts. The registry already infers the subclass, so the casts were masking what would otherwise be a typing regression. Typecheck confirms the inference stands on its own.

Tests grew 236 → 240, covering: unknown-key rejection, instance-not-class rejection, no-construction-before-validation, and cross-copy recognition where instanceof provably fails.

Checks run

Check Result
pnpm build ✅ 68/68
pnpm typecheck ✅ 143/143
pnpm lint ✅ 82/82
pnpm lint:deps ✅ no violations
pnpm test:packages ✅ 1000 files / 13289 tests
pnpm test:e2e ✅ 20 files / 109 tests
@prisma-next/mongo-orm ✅ 240 tests, no type errors
@prisma-next/mongo (incl. real replica-set e2e) ✅ 109 tests
pnpm test:integration ⚠️ 1 pre-existing flake, below

The previously red E2E Tests check passes locally after the main merge (20/20) — that failure was stale.

Two flakes seen locally, both unrelated to this diff (it touches no SQL or LSP code):

  • test/sql-orm-client/* — PGlite portal "C_N" does not exist, a different file each run (5 failures one run, 1 the next).
  • language-server/test/server.test.ts — teardown race under parallel load; passes in isolation (228/228).

Remaining risks / decisions for human review

  1. Wrong-model registration is still unchecked. collections: { User: TaskRepository } (where TaskRepository extends Collection<Contract, 'Task'>) type-checks, and client.users becomes a statically-TaskRepository collection constructed over the User model — Task-shaped types over User documents, no error anywhere. The SQL ORM shares this hole, so it is not a regression, but the brand advertises a safety it does not deliver here. Tightening it means asserting InstanceType<Collections[K]> extends MongoCollection<TContract, RootModelName<…>> in RootCollection — worth doing, but as a deliberate cross-family change.
  2. The "subclasses must keep the base constructor signature" constraint is documented but not enforced. A subclass adding a required constructor parameter silently receives undefined for it on every #clone. Not tested; a pinning test would be cheap if we want it nailed down.
  3. Static-typing asymmetry vs SQL. Mongo's where/take/skip/orderBy/select return this, so the subclass survives a chain statically. SQL's return the base Collection (runtime preserves the subclass; the static type does not). Mongo's behaviour is nicer, but ADR 175's eventual extraction has to reconcile the two — flagging rather than silently widening the gap.
  4. Dual surface. Keeping both the MongoCollection interface and the now-public Collection class is maintenance SQL doesn't carry (implements only enforces sync one way). Worth collapsing when the shared interface is extracted per ADR 175.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/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

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca6542 and 314fab7.

📒 Files selected for processing (4)
  • packages/2-mongo-family/5-query-builders/orm/src/collection.ts
  • packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts
  • packages/2-mongo-family/5-query-builders/orm/src/mongo-orm.ts
  • packages/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

Comment on lines +22 to +30
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>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.ts

Repository: 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 -n

Repository: 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')

PY

Repository: 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 -n

Repository: 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 -n

Repository: 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.

Comment on lines +159 to +170
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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
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.

1 participant