diff --git a/apps/docs/content/docs/guides/next/meta.json b/apps/docs/content/docs/guides/next/meta.json index 6f57759d5a..535252ee7a 100644 --- a/apps/docs/content/docs/guides/next/meta.json +++ b/apps/docs/content/docs/guides/next/meta.json @@ -3,6 +3,7 @@ "pages": [ "index", "frameworks", - "runtimes" + "runtimes", + "upgrade-prisma-orm" ] } diff --git a/apps/docs/content/docs/guides/next/upgrade-prisma-orm/meta.json b/apps/docs/content/docs/guides/next/upgrade-prisma-orm/meta.json new file mode 100644 index 0000000000..94beb121a0 --- /dev/null +++ b/apps/docs/content/docs/guides/next/upgrade-prisma-orm/meta.json @@ -0,0 +1,6 @@ +{ + "title": "Upgrade Prisma ORM", + "pages": [ + "mongodb" + ] +} diff --git a/apps/docs/content/docs/guides/next/upgrade-prisma-orm/mongodb.mdx b/apps/docs/content/docs/guides/next/upgrade-prisma-orm/mongodb.mdx new file mode 100644 index 0000000000..0565e641cf --- /dev/null +++ b/apps/docs/content/docs/guides/next/upgrade-prisma-orm/mongodb.mdx @@ -0,0 +1,452 @@ +--- +title: MongoDB +description: Migrate a MongoDB project from Prisma v6 to Prisma Next +url: /guides/next/upgrade-prisma-orm/mongodb +metaTitle: How to migrate a MongoDB project from Prisma v6 to Prisma Next +metaDescription: Step-by-step guide to migrating a Prisma ORM v6 MongoDB project to Prisma Next, covering setup, contracts, client calls, migrations, and production cutover. +--- + +This guide is for developers moving a Prisma ORM v6 MongoDB project to **Prisma Next**. Prisma 7 has **no MongoDB connector**, so Prisma Next is the successor path. This is a **code and workflow migration against the same database**: your data never moves. + +:::tip[Working with an AI coding agent?] + +Install the companion [prisma-mongodb-upgrade](https://github.com/prisma/skills/tree/main/prisma-mongodb-upgrade) skill with `npx skills add prisma/skills` to help your agent with this migration. + +::: + +## Prerequisites + +- **Node.js 24+** +- **TypeScript 5.9+** +- **MongoDB 8.0+** +- **The mongodb node driver 7.x** as a peer dependency +- **A replica set** if your app uses [transactions](/orm/prisma-client/queries/transactions). Prisma Next's ORM doesn't wrap `$transaction` yet, so multi-document atomicity goes through the driver's sessions (see step 3d), which MongoDB only allows on a replica set. + +:::info + +Prisma Next MongoDB is Early Access (GA planned after Postgres) and evolves quickly, so check anything below against the `@prisma-next/*` version you install. Every command and code example in this guide was validated against `@prisma-next/mongo@0.16.0`. + +::: + +:::warning + +The `mongodb@^7` driver no longer accepts AWS credentials embedded in the connection string. See the [driver v7 breaking changes](https://github.com/mongodb/node-mongodb-native/blob/HEAD/etc/notes/CHANGES_7.0.0.md#-aws-authentication). + +::: + +## 1. Set up Prisma Next + +Scaffold the project: + +```bash +npx prisma-next init --yes --target mongodb --authoring psl +``` + +`init` writes a `prisma-next.config.ts` at the repo root. MongoDB is selected by importing the `@prisma-next/mongo` façade, not by a `provider` string in the schema. Point the connection at the **same database** your v6 app uses. + +```typescript tab="After" +// prisma-next.config.ts +import 'dotenv/config'; +import { defineConfig } from '@prisma-next/mongo/config'; + +export default defineConfig({ + contract: './src/prisma/contract.prisma', + db: { + connection: process.env['DATABASE_URL']!, + }, +}); +``` + +```prisma tab="Before" +// schema.prisma +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +// Rest of schema... +``` + +### Generate the contract + +Run `npx prisma-next contract emit` to generate `contract.json` from your `.prisma` contract file. Re-run this whenever you change the contract. + +### Import the client + +The way to instantiate the Prisma client now is from the emitted contract: + +```typescript tab="After" +// src/prisma/db.ts +import mongo from '@prisma-next/mongo/runtime'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +const db = mongo({ + contractJson, + url: process.env['DATABASE_URL']!, + dbName: 'my-app-db', +}); + +export { db }; +``` + +```typescript tab="Before" +// src/prisma/db.ts +import { PrismaClient } from '../generated/prisma/client'; + +const prisma = new PrismaClient(); + +export { prisma }; +``` + +## 2. Port the schema to a contract + +:::note + +Prisma Next addresses collections by **storage name**, not model name: `db.orm.users` (the `@@map` name, or the lowercased model name), never `db.orm.User`. Whatever you `@@map` in this step is the exact name every client call uses in step 3. + +::: + +The v6 `schema.prisma` is not consumed as-is; it becomes a **contract**, authored in PSL (shown here) or TypeScript. The syntax is close to v6, with a few differences. + +In the table below, the left column is how you wrote it in v6 and the right column is the Prisma Next equivalent: + +:::tip + +You can use the [prisma-mongodb-upgrade](https://github.com/prisma/skills/tree/main/prisma-mongodb-upgrade) skill to help your agent map the contract to your schema. + +::: + +| Concept | v6 (`schema.prisma`) | Prisma Next (`contract.prisma`) | +|---------|----------------------|---------------------------------| +| Datasource | `datasource db { provider = "mongodb" }` | no `provider` (configured in `prisma-next.config.ts`) | +| Id field | `id String @id @default(auto()) @map("_id") @db.ObjectId` | `id ObjectId @id @map("_id")` | +| Embedded document | `type Address { ... }` | `type Address { ... }` (unchanged) | +| Index | `@@index(...)`, synced by `db push` | `@@index(...)` / `@@unique(...)`, applied by migrations | +| Polymorphism | not supported | `@@discriminator(field)` on the base + `@@base(Base, "value")` on each variant.
See [Base models and variants](/orm/next/contract-authoring/psl-syntax#base-models-and-variants) | + +A fuller contract putting together enum, embedded type, relation, and polymorphism: + +```prisma +// src/prisma/contract.prisma +enum UserRole { + @@type("mongo/string@1") + Admin = "admin" + Author = "author" + Reader = "reader" +} + +type Address { + street String + city String + country String +} + +model User { + id ObjectId @id @map("_id") + email String + role UserRole + address Address? + posts Post[] + @@map("users") +} + +model Post { + id ObjectId @id @map("_id") + title String + kind String + authorId ObjectId + createdAt DateTime + author User @relation(fields: [authorId], references: [id]) + @@discriminator(kind) + @@index([createdAt(sort: Desc), authorId]) + @@map("posts") +} + +model Article { + summary String + @@base(Post, "article") +} +``` + +When you use `@@discriminator`, declare a variant for every value the field takes in your existing data: the generated validator only accepts declared values, so writes to documents with an undeclared value fail once the validators are live. A variant model must declare at least one field (an optional one is enough). + +## 3. Port client calls + +Names look similar but parity does not hold. This section breaks down each category of operation. + +### 3a. Basic CRUD + +Prisma Next uses a chained API instead of v6's single options object: you start from a collection (like `db.orm.users`), add filters and options step by step, then finish with a method that runs the query: + +```typescript tab="After" +import { db } from './db'; + +// Find one +await db.orm.users.where({ email: 'alice@example.com' }).first(); + +// Create +const user = await db.orm.users.create({ email: 'a@e.com', role: 'author', address: null }); + +// Update one +await db.orm.users.where({ _id: user._id }).update({ role: 'author' }); + +// Update many +await db.orm.users.where({ role: 'reader' }).updateAll({ role: 'author' }); + +// Delete one +await db.orm.users.where({ _id: user._id }).delete(); +``` + +```typescript tab="Before" +import { prisma } from './db'; + +// Find one +await prisma.user.findFirst({ where: { email } }); + +// Create +const user = await prisma.user.create({ data: { email, role: 'author' } }); + +// Update one +await prisma.user.update({ where: { id }, data: { role: 'author' } }); + +// Update many +await prisma.user.updateMany({ where: { role: 'reader' }, data: { role: 'author' } }); + +// Delete one +await prisma.user.delete({ where: { id } }); +``` + +**Common mistakes:** + +| Don't | Do | Why | +|-------|----|-----| +| `db.orm.User` | `db.orm.users` | address by storage name, not model name | +| `.where({ email: { equals: 'x' } })` | `.where({ email: 'x' })` | `.where(...)` takes a plain equality object | +| `.update({ ... })` with no `.where(...)` | `.where(...).update({ ... })` | every mutation except `create` needs a `.where(...)` first | +| `role: 'Author'` | `role: 'author'` | enums read and write their storage value, not the key name | + +### 3b. Relations and polymorphism + +```typescript tab="After" +import { db } from './db'; + +// Eager-load a relation +const withAuthor = await db.orm.posts.include('author').all(); + +// Query one variant of a polymorphic collection +const articles = await db.orm.posts.variant('Article').all(); +``` + +```typescript tab="Before" +import { prisma } from './db'; + +// Eager-load a relation +const withAuthor = await prisma.post.findMany({ include: { author: true } }); + +// Query one variant of a polymorphic collection +const articles = await prisma.post.findMany({ where: { kind: 'article' } }); +``` + +### 3c. Aggregations + +v6's `groupBy` / `aggregate` become a typed pipeline: build a plan with `db.query`, then run it with `db.execute`. `acc` provides the group totals (`acc.count()`, `acc.max(...)`, and friends), the equivalent of v6's `_count` / `_sum` / `_max`. + +```typescript tab="After" +import { acc } from '@prisma-next/mongo-query-builder'; + +const plan = db.query + .from('posts') + .group((f) => ({ _id: f.kind, postCount: acc.count(), latest: acc.max(f.createdAt) })) + .sort({ postCount: -1 }) + .build(); +const byKind = await db.execute(plan).toArray(); +``` + +```typescript tab="Before" +await prisma.post.groupBy({ + by: ['kind'], + _count: { _all: true }, + _max: { createdAt: true }, + orderBy: { _count: { kind: 'desc' } }, +}); +``` + +:::warning[ObjectId filters need a raw command] + +Don't add `.match((f) => f.authorId.eq(authorId))` to filter by an `ObjectId` field (`_id`, or a foreign key like `authorId`): the typed builder sends the id through as a plain string, so the stage silently matches nothing. Filter ObjectId fields with the ORM client instead (`db.orm.posts.where({ authorId })` encodes them for you), or run the aggregation as a raw command carrying a real `ObjectId`: + +```typescript +import { RawAggregateCommand } from '@prisma-next/mongo-query-ast/execution'; +import { ObjectId } from 'mongodb'; + +const plan = db.query.rawCommand( + new RawAggregateCommand('posts', [ + { $match: { authorId: new ObjectId(String(authorId)) } }, + { $group: { _id: '$kind', postCount: { $count: {} }, latest: { $max: '$createdAt' } } }, + { $sort: { postCount: -1 } }, + ]), +); +const byKind = await db.execute(plan).toArray(); +``` + +::: + +### 3d. Transactions + +Prisma Next has no `transaction` method for MongoDB yet. For multi-document transactions, use the `mongodb` driver's [Transaction API](https://www.mongodb.com/docs/drivers/node/current/crud/transactions/transaction-conv/). + +```typescript tab="After" +import { MongoClient } from 'mongodb'; + +const client = new MongoClient(process.env['DATABASE_URL']!); +const session = client.startSession(); +const mdb = client.db('my-app-db'); + +try { + await session.withTransaction(async () => { + // Pass { session } to EVERY operation: an op without it silently + // runs outside the transaction. + await mdb + .collection('users') + .insertOne({ email: 'a@e.com', role: 'author' }, { session }); + await mdb + .collection('posts') + .updateOne({ _id: postId }, { $set: { authorId } }, { session }); + }); +} finally { + await session.endSession(); +} +``` + +```typescript tab="Before" +await prisma.$transaction(async (tx) => { + // ...writes... +}); +``` + +### 3e. Raw operations + +#### Aggregations + +The direct replacement for v6's `findRaw` and `aggregateRaw` is `db.raw`. + +```typescript tab="After" +import { db } from './db'; + +// Raw aggregation on a collection, replaces v6's aggregateRaw / findRaw. +const plan = db.raw + .collection('posts') + .aggregate([ ... ]) + .build(); +const rows = await db.execute(plan).toArray(); +``` + +```typescript tab="Before" +import { prisma } from './db'; + +// Raw aggregation on a collection. +const rows = await prisma.post.aggregateRaw({ + pipeline: [ ... ], +}); +``` + +#### Commands + +`db.raw` is collection-scoped, so v6's database-level `$runCommandRaw` has no ORM equivalent. Construct your own `MongoClient` and pass it to the binding from step 1 in place of `url`: the same client then powers the typed ORM and runs arbitrary commands, sharing one connection pool. + +```typescript tab="After" +// src/prisma/db.ts +import mongo from '@prisma-next/mongo/runtime'; +import { MongoClient } from 'mongodb'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +export const mongoClient = new MongoClient(process.env['DATABASE_URL']!); +export const db = mongo({ contractJson, mongoClient, dbName: 'my-app-db' }); + +// Elsewhere: typed ORM and raw commands from the same client. +await db.orm.users.where({ email: 'alice@example.com' }).first(); +await mongoClient.db('my-app-db').command({ ping: 1 }); +``` + +```typescript tab="Before" +import { prisma } from './db'; + +await prisma.$runCommandRaw({ ping: 1 }); +``` + +For raw reads and writes scoped to a single collection (anything the typed builder can't express), you don't need the driver: `db.query.rawCommand(...)` packages a raw command into a plan, as shown in [3c](#3c-aggregations). See [Raw queries](/orm/next/reference/raw-queries) for the full surface. + +### Quick reference table + +| v6 | Prisma Next | +|----|-------------| +| `prisma.user.findMany(...)` | `db.orm.users.where({ ... }).all()` | +| `prisma.user.findFirst(...)` | `db.orm.users.where({ ... }).first()` | +| `create` / `update` / `delete` / `upsert` | same names on `db.orm.` | +| `updateMany` / `deleteMany` | `updateAll` / `deleteAll` | +| `aggregate` / `groupBy` | `db.query.from(...).group(...).build()` → `db.execute(plan)` | +| `findRaw` / `aggregateRaw` | `db.raw.collection(...).aggregate(...).build()` → `db.execute(plan)`, or `db.query.rawCommand(...)` | +| `$runCommandRaw` | share a `MongoClient` with the binding, then `mongoClient.db(...).command(...)` (see 3e) | +| `$transaction(...)` | no wrapper yet; driver sessions on a replica set (see 3d) | +| `$connect` / `$disconnect` | lazy connect on first use; `db.close()` to disconnect | + +## 4. Adopt the migration lifecycle + +v6 MongoDB had no Prisma Migrate, only `db push` (ephemeral, no history). Prisma Next gives MongoDB first-class, contract-driven migrations: reviewable diffs you commit, reproducible across environments, and an auditable schema history. + +### Bring the v6 database under management + +Your database already has collections and data, so bootstrap it with `db update`: it diffs the live database against your contract, applies the delta (the strict validators, plus any indexes the contract declares that v6 never created), and signs the database so Prisma Next recognizes it from then on. The `--advance-ref db` flag records the resulting contract state as the [`db` ref](/orm/next/migrations/the-migration-graph#name-important-states-with-refs), the starting point later migration plans diff from. + +```bash +npx prisma-next contract emit +npx prisma-next db update --db "$DATABASE_URL" --dry-run # preview the delta first +npx prisma-next db update --db "$DATABASE_URL" --advance-ref db +``` + +Don't reach for `db init` here. It bootstraps only additively, and on a populated database it refuses the validator changes this migration needs, because adding a validator to a collection with existing documents is classed destructive. `db update` applies them after a confirmation prompt. + +### Every schema change after that + +Use `db update` for local experiments (the `db push` analogue, no history kept) and the migration workflow for shared branches and production: + +```bash +npx prisma-next migration plan --name add_posts_indexes # plans from the db ref +npx prisma-next migrate --db "$DATABASE_URL" --advance-ref db +npx prisma-next db verify --db "$DATABASE_URL" # check the DB matches the contract +``` + +**Good to know:** + +- You never hand-write migration steps: declare indexes and validators in the contract (step 2) and `migration plan` derives the changes. +- Interrupted `migrate` runs are resumable; just rerun. After fixing anything by hand, run `db sign` so the signature matches the database again. +- Prisma Next adds strict `$jsonSchema` validators by default. Make sure existing documents pass them before running in production: once the validators are live, writes to documents that don't match the contract fail with `Document failed validation`. + +## 5. Pre-flight checklist (before production cutover) + +Work through this list before you switch production traffic over: + +- [ ] **Same database.** Your Prisma Next config points at the **same** database and connection string as v6. +- [ ] **Server version.** Your MongoDB server is on 8.0 or newer. +- [ ] **Dry run first.** Rehearse the whole flow on a throwaway copy of your database (`contract emit`, `db update --dry-run`, `db update`, `db verify`) and make sure `db verify` passes before you touch production. +- [ ] **Index parity.** The indexes on each collection (`db.collection.getIndexes()`) match your contract. +- [ ] **Validators.** Your existing documents pass the strict validators Prisma Next adds to each collection (see *Good to know* in step 4). +- [ ] **Addressing.** Every call site uses storage names like `db.orm.users`, not model names. +- [ ] **Transactions.** Every v6 `$transaction` now has a driver-session equivalent. +- [ ] **Raw calls.** Every `$runCommandRaw`, `findRaw`, and `aggregateRaw` has a pipeline-builder or `mongodb`-driver replacement. +- [ ] **Read-only trial run.** Run Prisma Next read-only alongside your v6 app for a while and compare the results. This surfaces any differences while v6 is still handling all the writes. +- [ ] **Cutover and rollback.** Switch writes over only once the trial run looks good, and keep the v6 branch deployable. Rolling back is just a code rollback since your data never moved. + +Don't delete the v6 client, schema, or dependencies until this checklist passes. + +## 6. Post-migration: adopting Next workflows + +Once you've cut over, this is the day-to-day workflow: + +- **Schema changes.** Edit the contract, then run `contract emit`, `migration plan`, and `migrate` (same loop from step 4). +- **Troubleshooting.** `db verify` checks the database against your contract, and `db sign` records any fixes you make by hand. +- **Performance.** Your aggregations now compile to native MongoDB pipelines, so it's worth benchmarking them against your old v6 raw calls. + +See the [Prisma Next docs](/orm/next) for [pipeline builder patterns](/orm/next/reference/pipeline-builder), [relation loading strategies](/orm/next/reference/orm-client), and [advanced contract features](/orm/next/contract-authoring/psl-syntax). This guide gets you across the bridge; Prisma Next's docs are the source of truth from here on.