{"id":"f65c12f2-4ab2-42b7-8058-cde5e0de36b1","slug":"prisma-next-queries-mongo","kind":"skill","name":"Prisma Next — Queries (Mongo)","summary":"Load this guide when db.ts imports from @prisma next/mongo/runtime .","owner":"prisma","repoUrl":"https://github.com/prisma/prisma-next/blob/HEAD/skills/prisma-next-queries/mongo.md","license":"Apache-2.0","licenseTier":"permissive","trust":"official","stars":415,"match":null,"fewStars":false,"format":"instructions","createdAt":null,"addedAt":"2026-10-14T11:52:09.385Z","logoUrl":"https://avatars.githubusercontent.com/u/17219288?v=4","linkOnly":false,"collection":{"name":"prisma-next","repo":"prisma/prisma-next"},"skillPath":"skills/prisma-next-queries/mongo.md","categories":["writing"],"tags":["loggy-core","loggy-terminal"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-14T11:52:09.382Z","sample":false,"url":"https://conduits.dev/tools/prisma-next-queries-mongo","language":"TypeScript","transports":[],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Prisma Next — Queries (Mongo)\n\n> Load this guide when `db.ts` imports from `@prisma-next/mongo/runtime`.\n\nShared concepts (result consumption, script teardown, cross-target pitfalls, capability gaps) live in [`SKILL.md`](./SKILL.md).\n\n## Key Concepts\n\n**Mongo** (`mongo<Contract>(...)` from `@prisma-next/mongo/runtime`):\n\n- **`db.orm.<root>`** — ORM, lowercased plural contract root (`db.orm.users`, `db.orm.posts`). Same fluent chaining; `.where({ field: value })` object equality is the idiomatic filter form.\n- **`db.query`** — typed aggregation-pipeline builder. Start with `db.query.from('<root>')`, chain `.match(...)` / `.project(...)` / `.group(...)` / `.lookup(...)`, terminal with `.build()`. Execute via `(await db.runtime()).execute(plan)`.\n\nReach for the ORM first; drop to `db.query` when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app.\n\n**Lane decision table:**\n\n| Need | Choose | Why |\n|---|---|---|\n| Standard CRUD with reference relations | **ORM (`db.orm.<root>`)** | Collection-shaped; object `.where({ ... })`; `.create` / `.update` / `.delete` / `.upsert`. |\n| Eager-load a reference relation | **ORM `.include('<relation>')`** | Lowers to `$lookup`; composes with `.where` / `.select` / `.orderBy` / `.take`. |\n| Polymorphic root (discriminated variants) | **ORM `.variant('<VariantName>')`** | Narrows to one variant and injects the discriminator filter. |\n| Field-level Mongo updates (`$push`, `$inc`, dot-path `$set`) | **ORM `.update((f) => [f.field.inc(1)])`** | Field-accessor callback; plain-object `.update({ ... })` for whole-field replacement. |\n| Aggregation pipeline (group, facet, `$lookup` with reshaping) | **Query builder (`db.query.from(...)`)** | Full pipeline surface; typed row shape through `.build()`. |\n| Typed cross-collection join in a pipeline | **Query builder `.lookup((from) => from('users').on(...).as('author'))`** | `$lookup` with compile-time foreign-root checking. |\n| Bulk writes with pipeline semantics | **Query builder write terminals** (`.insertOne`, `.updateMany`, `.findOneAndUpdate`, `.upsertOne`, …) | Filtered writes after `.match(...)`; plans execute through the runtime. |\n\n## Workflow — ORM reads\n\nThe concept matches Postgres — `db.orm.<root>` returns a collection you compose method-by-method — but roots are **lowercased plurals** from the emitted contract (`users`, `posts`, not `User` / `Post`), and filters are usually **object equality**:\n\n```typescript\n// src/queries/users.ts — adjust the relative import to match file depth.\nimport { db } from '../prisma/db';\n\n// All users.\nconst users = await db.orm.users.all();\n\n// Single row by equality filter.\nconst alice = await db.orm.users.where({ email: 'alice@example.com' }).first();\n\n// Projection, sort, pagination — same chaining as Postgres.\nconst recent = await db.orm.posts\n  .select('title', 'authorId', 'createdAt')\n  .orderBy({ createdAt: -1 })\n  .take(10)\n  .all();\n```\n\n**`.where(...)`** accepts a plain object whose keys are model field names and values are compared with equality (codec-aware — `ObjectId` fields accept string ids from the contract). Chain multiple `.where({ ... })` calls to AND-compose filters.\n\nFor operators the object form doesn't cover (`.in([...])`, range comparisons, nested logic), pass a `MongoFilterExpr` — today that means importing filter helpers from `@prisma-next/mongo-query-ast/execution` (a façade-completeness gap; see *What Prisma Next doesn't do yet* in [`SKILL.md`](./SKILL.md)). Prefer the object form whenever equality suffices.\n\n**Polymorphic roots.** When the contract declares variants on a model, narrow before querying:\n\n```typescript\nconst articles = await db.orm.posts.variant('Article').all();\nconst tutorials = await db.orm.posts.variant('Tutorial').where({ authorId }).all();\n```\n\n**Sorting and pagination.** `.orderBy({ field: 1 | -1 })` (Mongo sort directions). `.take(n)` maps to `$limit`; `.skip(n)` maps to `$skip`.\n\n**`.first()` vs `.all()`.** `.first()` issues a limit-1 read; `.all()` returns every matching document. There is no `.first({ pk })` shorthand on Mongo — filter on `_id` explicitly: `.where({ _id: id }).first()`.\n\nMongo `.all()` returns the same `AsyncIterableResult` shape as Postgres — `await db.orm.users.all()` yields an array; see *Consuming the result* in [`SKILL.md`](./SKILL.md).\n\n## Workflow — Eager-loading relations (`.include`)\n\nMongo reference relations eager-load through the same `.include('<relation>')` surface; the ORM lowers to `$lookup`:\n\n```typescript\nconst posts = await db.orm.posts\n  .include('author')\n  .orderBy({ createdAt: -1 })\n  .all();\n// → Array<{ title, authorId, createdAt, author: { name, email, ... } }>\n```\n\nRelation names match the contract's `@relation` field names. Nested includes follow the same chaining rules as the parent collection.\n\n## Workflow — ORM writes\n\nMongo mutations require a preceding `.where(...)` filter (except `.create` / `.createAll`). Updates accept either a partial document or a field-accessor callback for Mongo operators:\n\n```typescript\n// Create — returns the row with server-assigned `_id`.\nconst user = await db.orm.users.create({\n  name: 'Alice',\n  email: 'alice@example.com',\n  bio: null,\n  address: null,\n});\n\n// Update one — plain object replaces top-level fields.\nawait db.orm.users.where({ _id: user._id }).update({ bio: 'Writer' });\n\n// Update one — field operations ($push, $inc, dot-path $set).\nawait db.orm.users\n  .where({ _id: user._id })\n  .update((u) => [u.tags.push('admin'), u.loginCount.inc(1)]);\n\n// Update many / delete many — iterate or count.\nconst updated = await db.orm.users\n  .where({ bio: null })\n  .updateAll({ bio: 'filled' });\nfor await (const row of updated) { /* each modified doc */ }\n\nawait db.orm.users.where({ _id: user._id }).delete();\n\n// Upsert — filter via .where(), split create vs update branches.\nawait db.orm.users.where({ email: 'alice@example.com' }).upsert({\n  create: { name: 'Alice', email: 'alice@example.com', bio: null, address: null },\n  update: { bio: 'Editor' },\n});\n```\n\n**Count-only terminals.** `.createAndCount(...)`, `.updateAndCount(...)`, `.deleteAndCount()` return numbers without re-reading full documents — useful for bulk operations where you only need the modified count.\n\n**Upsert + dot-path.** The upsert `update` callback cannot use dot-path field operations — use top-level field replacement in the upsert branch or a separate `.update((u) => [...])` call.\n\n## Workflow — Aggregates\n\nThe Mongo ORM does not expose `.aggregate(...)` / `.groupBy(...)`. Express aggregations through **`db.query`** — the pipeline builder — with `.group(...)` and accumulator helpers:\n\n```typescript\nimport { acc } from '@prisma-next/mongo-query-builder';\n\nconst runtime = await db.runtime();\nconst plan = db.query\n  .from('posts')\n  .match((f) => f.authorId.eq(authorId))\n  .group((f) => ({\n    _id: f.kind,\n    postCount: acc.count(),\n    latest: acc.max(f.createdAt),\n  }))\n  .sort({ postCount: -1 })\n  .build();\n\nconst byKind = await runtime.execute(plan);\n```\n\nImport `acc` and expression helpers (`fn`) from `@prisma-next/mongo-query-builder` when building computed pipeline stages.\n\n## Workflow — Query builder (`db.query`)\n\nThe concept: `db.query.from('<root>')` starts a typed aggregation-pipeline chain. Terminal methods produce a `MongoQueryPlan`; execute it through the runtime:\n\n```typescript\n// src/queries/analytics.ts\nimport { acc, fn } from '@prisma-next/mongo-query-builder';\nimport { db } from '../prisma/db';\n\nconst runtime = await db.runtime();\n\n// Read pipeline — match, project, sort, limit.\nconst plan = db.query\n  .from('posts')\n  .match((f) => f.authorId.eq(authorId))\n  .sort({ createdAt: -1 })\n  .limit(10)\n  .project('title', 'authorId', 'createdAt')\n  .build();\nconst recent = await runtime.execute(plan);\n\n// Cross-collection join ($lookup).\nconst withAuthor = db.query\n  .from('posts')\n  .lookup((from) =>\n    from('users')\n      .on((local, foreign) => ({\n        local: local.authorId,\n        foreign: foreign._id,\n      }))\n      .as('author'),\n  )\n  .build();\nconst rows = await runtime.execute(withAuthor);\n```\n\n**Filters — `.match(...)`.** Callback form: `.match((f) => f.status.eq('active'))`. Filters AND-compose across chained `.match(...)` calls. Field accessors support property access (`f.email`), callable dot paths (`f('address.city').eq('NYC')`), and `f.rawPath('path')` for migration/backfill paths outside the current contract.\n\n**Write terminals on the builder.** After `.from('users')` or `.from('users').match(...)`, use insert/update/delete terminals:\n\n```typescript\nawait runtime.execute(\n  db.query.from('users').insertOne({ name: 'Alice', email: 'a@e.com', bio: null }),\n);\n\nawait runtime.execute(\n  db.query\n    .from('users')\n    .match((f) => f.name.eq('Alice'))\n    .updateMany((f) => [f.bio.set('filled')]),\n);\n\nawait runtime.execute(\n  db.query\n    .from('users')\n    .match((f) => f.email.eq('a@e.com'))\n    .findOneAndUpdate((f) => [f.bio.set('updated')], { returnDocument: 'after' }),\n);\n```\n\nUpdate callbacks return arrays of field operations (`.set`, `.inc`, `.push`, `.pull`, …). Pipeline-style updates use `f.stage.set(...)` inside an aggregation chain, then `.updateMany()` with no callback.\n\n**Plans vs ORM.** The ORM's `.create` / `.update` / `.all` issue queries directly. Don't pass ORM collections to `runtime.execute` — that entry point is for `db.query` plans (and migration/runtime internals).\n\n## Common Pitfalls (Mongo)\n\n1. **Reaching for the lower-level lane when the ORM would have done.** Default to the ORM; drop to `db.query` only for shapes the ORM can't express.\n2. **Using `.all()` when you wanted one row.** Use `.where({ ... }).first()` — not `.all()`.\n3. **Calling `.update()` / `.delete()` without `.where()`.** Mutations other than `.create` / `.createAll` require a filter — the compiler enforces this at the type level where possible.\n4. **Using PascalCase model names on ORM.** Roots are lowercased plurals from the contract (`db.orm.users`, not `db.orm.User`).\n5. **Expecting Postgres-style lambda `.where((u) => u.email.eq(...))` on ORM.** Prefer object equality `.where({ email: '...' })`; richer operators need `MongoFilterExpr` helpers (façade gap today).\n6. **Expecting `db.transaction(...)`.** The Mongo façade does not expose it today. Multi-document atomicity requires MongoDB transactions on a replica set via the driver — not yet wrapped in the Prisma Next façade. Route to *What Prisma Next doesn't do yet* / `prisma-next-feedback` if the user needs this.\n7. **Trying to use `db.sql`.** There is no `db.sql` on Mongo.\n8. **Trying to `db.execute(plan)` directly.** Execute query-builder plans via `(await db.runtime()).execute(plan)`.\n9. **Expecting ORM `.aggregate(...)` / `.groupBy(...)`.** Use `db.query.from(...).group(...).build()` instead.\n\n## Reference Files\n\n- Example queries under [`examples/mongo-demo/src/server.ts`](examples/mongo-demo/src/server.ts) — ORM reads, `.include`, `.variant`, and pipeline DSL via `db.query`.\n- Integration tests under `examples/mongo-demo/test/` (`blog.test.ts`, `crud-lifecycle.test.ts`, `query-builder-writes.test.ts`).\n- Query builder README under `packages/2-mongo-family/5-query-builders/query-builder/README.md`.\n- ORM collection surface under `packages/2-mongo-family/5-query-builders/orm/src/collection.ts`.\n\n## Checklist\n\n- [ ] Used lowercased plural ORM roots (`db.orm.users`, not `db.orm.User`).\n- [ ] Chose the right lane (ORM by default; `db.query` for shapes the ORM doesn't express).\n- [ ] Used `.where({ ... }).first()` for single-row reads — not `.all()`.\n- [ ] Executed query-builder plans via `(await db.runtime()).execute(plan)`.\n- [ ] For aggregations, used `db.query.from(...).group(...)` rather than a non-existent ORM `.aggregate(...)`.\n- [ ] Did NOT confabulate `db.transaction`, `db.sql`, or ORM `.aggregate(...)` — routed to *What Prisma Next doesn't do yet* / `prisma-next-feedback` instead.\n- [ ] Did NOT use the lower-level builder for something the ORM cleanly expresses."}