[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71NJZQASECJNGTKXSYDWKW9":3,"skill-same:01M71NJZQASECJNGTKXSYDWKW9":83},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":15,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":55,"commitSha":56,"skillDirectory":57,"forksCount":58,"repositoryHtmlUrl":59,"repositoryHomepageUrl":60,"ownerHtmlUrl":61,"ownerBlogUrl":62,"ownerDescription":6,"updatedAt":63,"install":64,"resources":77},"01M71NJZQASECJNGTKXSYDWKW9","prisma-patterns",null,"Use when designing Prisma schemas or writing Prisma queries, transactions, pagination or migrations in a TypeScript backend. Flags traps like updateMany returning a count, $transaction timeouts, migrate dev resetting the DB and @updatedAt skipped on bulk updates, plus serverless and soft-delete patterns.","monoes\u002Fmonomind","monoes","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F276744696?v=4","01M71KWF37AJ2NJBP3KWBVSDGT","01M71KWF4CSF8ADH0M6HA09VD2",23,"MIT","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fprisma-patterns\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fprisma-patterns\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"patterns","prisma patterns","prisma","queries","transaction","use","dev","count","data","flags","typescript","delete","transactions","migrate","updates","when","database","writing","db","bulk","2026-11-10 10:56:23",false,"---\nname: prisma-patterns\ndescription: \"Use when designing Prisma schemas or writing Prisma queries, transactions, pagination or migrations in a TypeScript backend. Flags traps like updateMany returning a count, $transaction timeouts, migrate dev resetting the DB and @updatedAt skipped on bulk updates, plus serverless and soft-delete patterns.\"\ntags: [\"data\",\"backend\",\"typescript\",\"database\",\"migrate\"]\ntools: []\nlicense: MIT\nsource: https:\u002F\u002Fgithub.com\u002Faffaan-m\u002FECC\nsource_path: \"skills\u002Fprisma-patterns\"\nsource_commit: bf70150eb2df8070024e5bdf08e4aa08959e2735\n---\n# Prisma Patterns\n\nProduction patterns and non-obvious traps for Prisma ORM in TypeScript backends.\n\n> **Check your version before applying patterns.** The Prisma API surface has evolved across major releases:\n>\n> ```bash\n> npx prisma --version\n> ```\n>\n> Notable API differences across versions:\n> - `relationJoins` can load relations via JOIN rather than separate queries, but may cause row explosion on large 1:N relations or deep `include` — benchmark both approaches\n> - `omit` field modifier and `prisma.$extends` Client Extensions API were added\n> - **Newer installs**: the package may be named `prisma` instead of `@prisma\u002Fclient`; `PrismaClient` may require a driver adapter (e.g. `@prisma\u002Fadapter-pg`); `datasource.url` may live in `prisma.config.ts` instead of `schema.prisma`\n> - CLI commands (`migrate dev`, `migrate deploy`, `generate`) are unchanged across versions\n\n## When to Activate\n\n- Designing or modifying Prisma schema models and relations\n- Writing queries, transactions, or pagination logic\n- Using `updateMany`, `deleteMany`, or any bulk operation\n- Running or planning database migrations\n- Deploying to serverless environments (Vercel, Lambda, Cloudflare Workers)\n- Implementing soft delete or multi-tenant row filtering\n\n## Core Concepts\n\n### ID Strategy\n\n| Strategy | Use When | Avoid When |\n|---|---|---|\n| `@default(cuid())` | Default choice — URL-safe, sortable, no collisions | Sequential IDs needed for external systems |\n| `@default(uuid())` | Interoperability with non-Prisma systems required | High-write tables (random UUIDs fragment B-tree indexes) |\n| `@default(autoincrement())` | Internal join tables, audit logs | Public-facing IDs (exposes record count) |\n\n### Schema Defaults\n\n```prisma\nmodel User {\n  id        String    @id @default(cuid())\n  email     String    @unique  \u002F\u002F @unique already creates an index — no @@index needed\n  name      String\n  role      Role      @default(USER)\n  posts     Post[]\n  createdAt DateTime  @default(now())\n  updatedAt DateTime  @updatedAt\n  deletedAt DateTime?\n\n  @@index([createdAt])\n  @@index([deletedAt, createdAt]) \u002F\u002F composite for soft-delete + sort queries\n}\n```\n\n- Add `@@index` on every foreign key and column used in `WHERE` or `ORDER BY`.\n- Declare `deletedAt DateTime?` upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.\n- `updatedAt @updatedAt` is set automatically by Prisma on `update` and `upsert` only (see Anti-Patterns for bulk update trap).\n\n### `include` vs `select`\n\n| | `include` | `select` |\n|---|---|---|\n| Returns | All scalar fields + specified relations | Only specified fields |\n| Use when | You need most fields plus a relation | Hot paths, large tables, avoiding over-fetch |\n| Performance | May over-fetch on wide tables | Minimal payload, faster on large datasets |\n| Prisma 5 note | Uses JOIN by default (`relationJoins`) | Same |\n\n```ts\n\u002F\u002F include — all columns + relation\nconst user = await prisma.user.findUnique({\n  where: { id },\n  include: { posts: { select: { id: true, title: true } } },\n});\n\n\u002F\u002F select — explicit allowlist\nconst user = await prisma.user.findUnique({\n  where: { id },\n  select: { id: true, email: true, name: true },\n});\n```\n\nNever return raw Prisma entities from API responses — map to response DTOs to control exposed fields:\n\n```ts\n\u002F\u002F BAD: leaks passwordHash, deletedAt, internal fields\nreturn await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F GOOD: explicit DTO mapping\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nreturn { id: user.id, name: user.name, email: user.email };\n```\n\n### Transaction Form Selection\n\n| Situation | Use |\n|---|---|\n| Independent operations, no inter-dependency | Array form |\n| Later step depends on earlier result | Interactive form |\n| External calls (email, HTTP) involved | Outside transaction entirely |\n\n```ts\n\u002F\u002F Array form — batched in one round trip\nconst [user, post] = await prisma.$transaction([\n  prisma.user.update({ where: { id }, data: { name } }),\n  prisma.post.create({ data: { title, authorId: id } }),\n]);\n\n\u002F\u002F Interactive form — use tx client only, never the outer prisma client\nconst post = await prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  if (user.role !== 'ADMIN') throw new Error('Forbidden');\n  return tx.post.create({ data: { title, authorId: user.id } });\n});\n```\n\n### PrismaClient Singleton\n\nEach `PrismaClient` instance opens its own connection pool. Instantiate once.\n\n```ts\n\u002F\u002F lib\u002Fprisma.ts\n\n\u002F\u002F Option A — adapter-based initialization (required by newer Prisma installs)\nimport { PrismaClient } from '@prisma\u002Fclient'; \u002F\u002F or the generated client path for your setup\nimport { PrismaPg } from '@prisma\u002Fadapter-pg';\n\nfunction createPrismaClient() {\n  const adapter = new PrismaPg({\n    connectionString: process.env.DATABASE_URL!,\n  });\n  return new PrismaClient({\n    adapter,\n    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],\n  });\n}\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nexport const prisma = globalForPrisma.prisma ?? createPrismaClient();\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n\n\u002F\u002F Option B — direct initialization (older installs, no adapter needed)\n\u002F\u002F import { PrismaClient } from '@prisma\u002Fclient';\n\u002F\u002F export const prisma = globalForPrisma.prisma ?? new PrismaClient({ ... });\n```\n\nUse Option A if your Prisma install requires an `adapter` argument in the `PrismaClient` constructor.\nUse Option B if `new PrismaClient()` works without arguments. Let the compiler tell you which is correct.\n\nThe `globalThis` pattern prevents duplicate instances during hot reload (Next.js, nodemon, ts-node-dev).\n\n### N+1 Problem\n\nLoading relations inside a loop issues one query per row.\n\n```ts\n\u002F\u002F BAD: N+1 — one extra query per user\nconst users = await prisma.user.findMany();\nfor (const user of users) {\n  const posts = await prisma.post.findMany({ where: { authorId: user.id } });\n}\n\n\u002F\u002F GOOD: single query\nconst users = await prisma.user.findMany({ include: { posts: true } });\n```\n\nWith Prisma 5+ `relationJoins`, the `include` form uses a single JOIN. On large 1:N sets this may increase result set size — benchmark both approaches if the relation can return many rows per parent.\n\n## Code Examples\n\n### Cursor Pagination (preferred for feeds and large datasets)\n\n```ts\nasync function getPosts(cursor?: string, limit = 20) {\n  const items = await prisma.post.findMany({\n    where: { published: true },\n    orderBy: [\n      { createdAt: 'desc' },\n      { id: 'desc' }, \u002F\u002F secondary sort prevents unstable pagination on duplicate timestamps\n    ],\n    take: limit + 1,\n    ...(cursor && { cursor: { id: cursor }, skip: 1 }),\n  });\n\n  const hasNextPage = items.length > limit;\n  if (hasNextPage) items.pop();\n\n  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };\n}\n```\n\nFetch `limit + 1` and pop — canonical way to detect `hasNextPage` without an extra count query. Always include a unique field (e.g. `id`) as a secondary `orderBy` to prevent unstable pagination when multiple rows share the same timestamp. Use offset pagination only when users need to jump to arbitrary pages (admin tables).\n\n### Soft Delete\n\n```ts\n\u002F\u002F Always filter explicitly — do not rely on middleware (hides behavior, hard to debug)\nconst activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });\n\nawait prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });\nawait prisma.user.update({ where: { id }, data: { deletedAt: null } }); \u002F\u002F restore\n```\n\n### Error Handling\n\n```ts\nimport { Prisma } from '@prisma\u002Fclient'; \u002F\u002F or the generated client path for your setup\n\ntry {\n  await prisma.user.create({ data: { email } });\n} catch (e) {\n  if (e instanceof Prisma.PrismaClientKnownRequestError) {\n    if (e.code === 'P2002') throw new ConflictError('Email already exists');\n    if (e.code === 'P2025') throw new NotFoundError('Record not found');\n    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');\n  }\n  throw e;\n}\n```\n\nCommon codes: `P2002` unique violation · `P2025` not found · `P2003` foreign key violation.\n\nCatch at the service boundary and translate to domain errors. Never expose raw Prisma messages to API consumers.\n\n### Connection Pool — Serverless\n\nEmbed connection params directly in `DATABASE_URL` — string concatenation breaks if the URL already has query parameters (e.g. `?schema=public`):\n\n```bash\n# .env — preferred: embed params in the URL\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?connection_limit=1&pool_timeout=20\"\n\n# With an external pooler (PgBouncer, Supabase pooler)\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?pgbouncer=true&connection_limit=1\"\n```\n\n```ts\n\u002F\u002F Vercel, AWS Lambda, and similar serverless runtimes:\n\u002F\u002F cap pool to 1 per instance; connection_limit and pool_timeout controlled via DATABASE_URL\n\n\u002F\u002F Adapter-based setup (if your Prisma install requires an adapter):\nimport { PrismaClient } from '@prisma\u002Fclient';\nimport { PrismaPg } from '@prisma\u002Fadapter-pg';\n\nconst prisma = new PrismaClient({\n  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),\n});\n\n\u002F\u002F Direct setup (if your Prisma install does not require an adapter):\n\u002F\u002F const prisma = new PrismaClient();\n```\n\n## Anti-Patterns\n\n### `updateMany` returns a count, not records\n\n```ts\n\u002F\u002F BAD: result is { count: 2 } — users[0] is undefined\nconst users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });\n\n\u002F\u002F GOOD: capture IDs first, then update, then fetch only the affected rows\nconst targets = await prisma.user.findMany({\n  where: { role: 'GUEST' },\n  select: { id: true },\n});\nconst ids = targets.map((u) => u.id);\nawait prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });\nconst updated = await prisma.user.findMany({ where: { id: { in: ids } } });\n```\n\nSame applies to `deleteMany` — returns `{ count: n }`, never the deleted rows.\n\n### `$transaction` interactive form times out after 5 seconds\n\n```ts\n\u002F\u002F BAD: external call inside transaction exceeds 5s default → \"Transaction already closed\"\nawait prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  await sendWelcomeEmail(user.email); \u002F\u002F external call\n  await tx.user.update({ where: { id }, data: { emailSent: true } });\n});\n\n\u002F\u002F GOOD: external calls outside the transaction\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nawait sendWelcomeEmail(user.email);\nawait prisma.user.update({ where: { id }, data: { emailSent: true } });\n\n\u002F\u002F Only raise timeout when bulk processing genuinely needs it\nawait prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });\n```\n\n### `migrate dev` can reset the database\n\n`migrate dev` detects schema drift and may prompt to reset the DB, dropping all data.\n\n```bash\n# NEVER on shared dev, staging, or production\nnpx prisma migrate dev --name add_column\n\n# Safe everywhere except local solo dev\nnpx prisma migrate deploy\n\n# Check drift without applying\nnpx prisma migrate diff \\\n  --from-migrations .\u002Fprisma\u002Fmigrations \\\n  --to-schema-datamodel .\u002Fprisma\u002Fschema.prisma \\\n  --shadow-database-url \"$SHADOW_DATABASE_URL\"\n```\n\n### Manually editing a migration file breaks future deploys\n\nPrisma checksums every migration file. Editing after apply causes `P3006 checksum mismatch` on every environment where the original already ran. Create a new migration instead.\n\n### Breaking schema changes require multi-step migration\n\nAdding `NOT NULL` to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:\n\n```bash\n# Step 1: create migration locally, then deploy\nnpx prisma migrate dev --name add_new_column   # local only\nnpx prisma migrate deploy                       # staging \u002F production\n```\n\n```ts\n\u002F\u002F Step 2: backfill data (run in a script or migration job, not in the shell)\nawait prisma.user.updateMany({ data: { newColumn: derivedValue } });\n```\n\n```bash\n# Step 3: create the NOT NULL constraint migration locally, then deploy\nnpx prisma migrate dev --name make_new_column_required  # local only\nnpx prisma migrate deploy                               # staging \u002F production\n```\n\n### `@updatedAt` does not fire on `updateMany`\n\n`@updatedAt` is set automatically only on `update` and `upsert`. Bulk writes leave it stale.\n\n```ts\n\u002F\u002F BAD: updatedAt stays at its old value\nawait prisma.post.updateMany({ where: { authorId }, data: { published: true } });\n\n\u002F\u002F GOOD\nawait prisma.post.updateMany({\n  where: { authorId },\n  data: { published: true, updatedAt: new Date() },\n});\n```\n\n### Soft delete + `findUniqueOrThrow` leaks deleted records\n\n`findUniqueOrThrow` throws `P2025` only when the row does not exist in the DB. Soft-deleted rows still exist and are returned without error.\n\n`findUniqueOrThrow` requires a unique constraint field in `where` — adding `deletedAt: null` alongside `id` breaks the type because `{ id, deletedAt }` is not a compound unique constraint. Use `findFirstOrThrow` instead.\n\n```ts\n\u002F\u002F BAD: returns soft-deleted user\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F BAD: Prisma type error — { id, deletedAt } is not a unique constraint\nconst user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });\n\n\u002F\u002F GOOD: findFirstOrThrow supports arbitrary where conditions\nconst user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });\n```\n\n### `deleteMany` without `where` deletes every row\n\n```ts\n\u002F\u002F BAD: silently wipes the table\nawait prisma.post.deleteMany();\n\n\u002F\u002F GOOD\nawait prisma.post.deleteMany({ where: { authorId: userId } });\n```\n\n## Best Practices\n\n| Rule | Reason |\n|---|---|\n| `migrate deploy` in CI\u002FCD, `migrate dev` only locally | `migrate dev` can reset the DB on drift |\n| Map entities to response DTOs | Prevents leaking internal fields |\n| Catch `PrismaClientKnownRequestError` at service boundary | Translate to domain errors |\n| Prefer `*OrThrow` methods over manual null checks | Throws P2025 automatically; use `findFirstOrThrow` when filtering non-unique fields |\n| `connection_limit=1` + external pooler in serverless | Prevents connection exhaustion |\n| Always provide `where` on `deleteMany` | Prevents accidental table wipe |\n| Set `updatedAt: new Date()` manually in `updateMany` | `@updatedAt` skips bulk writes |\n\n## Related Skills\n\n- `nestjs-patterns` — NestJS service layer that integrates Prisma\n- `postgres-patterns` — PostgreSQL-level indexing and connection tuning\n- `database-migrations` — multi-step migration planning for production\n- `backend-patterns` — general API and service layer design\n","# Prisma Patterns\n\nProduction patterns and non-obvious traps for Prisma ORM in TypeScript backends.\n\n> **Check your version before applying patterns.** The Prisma API surface has evolved across major releases:\n>\n> ```bash\n> npx prisma --version\n> ```\n>\n> Notable API differences across versions:\n> - `relationJoins` can load relations via JOIN rather than separate queries, but may cause row explosion on large 1:N relations or deep `include` — benchmark both approaches\n> - `omit` field modifier and `prisma.$extends` Client Extensions API were added\n> - **Newer installs**: the package may be named `prisma` instead of `@prisma\u002Fclient`; `PrismaClient` may require a driver adapter (e.g. `@prisma\u002Fadapter-pg`); `datasource.url` may live in `prisma.config.ts` instead of `schema.prisma`\n> - CLI commands (`migrate dev`, `migrate deploy`, `generate`) are unchanged across versions\n\n## When to Activate\n\n- Designing or modifying Prisma schema models and relations\n- Writing queries, transactions, or pagination logic\n- Using `updateMany`, `deleteMany`, or any bulk operation\n- Running or planning database migrations\n- Deploying to serverless environments (Vercel, Lambda, Cloudflare Workers)\n- Implementing soft delete or multi-tenant row filtering\n\n## Core Concepts\n\n### ID Strategy\n\n| Strategy | Use When | Avoid When |\n|---|---|---|\n| `@default(cuid())` | Default choice — URL-safe, sortable, no collisions | Sequential IDs needed for external systems |\n| `@default(uuid())` | Interoperability with non-Prisma systems required | High-write tables (random UUIDs fragment B-tree indexes) |\n| `@default(autoincrement())` | Internal join tables, audit logs | Public-facing IDs (exposes record count) |\n\n### Schema Defaults\n\n```prisma\nmodel User {\n  id        String    @id @default(cuid())\n  email     String    @unique  \u002F\u002F @unique already creates an index — no @@index needed\n  name      String\n  role      Role      @default(USER)\n  posts     Post[]\n  createdAt DateTime  @default(now())\n  updatedAt DateTime  @updatedAt\n  deletedAt DateTime?\n\n  @@index([createdAt])\n  @@index([deletedAt, createdAt]) \u002F\u002F composite for soft-delete + sort queries\n}\n```\n\n- Add `@@index` on every foreign key and column used in `WHERE` or `ORDER BY`.\n- Declare `deletedAt DateTime?` upfront when soft delete is a foreseeable requirement — adding it later requires a migration on a live table.\n- `updatedAt @updatedAt` is set automatically by Prisma on `update` and `upsert` only (see Anti-Patterns for bulk update trap).\n\n### `include` vs `select`\n\n| | `include` | `select` |\n|---|---|---|\n| Returns | All scalar fields + specified relations | Only specified fields |\n| Use when | You need most fields plus a relation | Hot paths, large tables, avoiding over-fetch |\n| Performance | May over-fetch on wide tables | Minimal payload, faster on large datasets |\n| Prisma 5 note | Uses JOIN by default (`relationJoins`) | Same |\n\n```ts\n\u002F\u002F include — all columns + relation\nconst user = await prisma.user.findUnique({\n  where: { id },\n  include: { posts: { select: { id: true, title: true } } },\n});\n\n\u002F\u002F select — explicit allowlist\nconst user = await prisma.user.findUnique({\n  where: { id },\n  select: { id: true, email: true, name: true },\n});\n```\n\nNever return raw Prisma entities from API responses — map to response DTOs to control exposed fields:\n\n```ts\n\u002F\u002F BAD: leaks passwordHash, deletedAt, internal fields\nreturn await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F GOOD: explicit DTO mapping\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nreturn { id: user.id, name: user.name, email: user.email };\n```\n\n### Transaction Form Selection\n\n| Situation | Use |\n|---|---|\n| Independent operations, no inter-dependency | Array form |\n| Later step depends on earlier result | Interactive form |\n| External calls (email, HTTP) involved | Outside transaction entirely |\n\n```ts\n\u002F\u002F Array form — batched in one round trip\nconst [user, post] = await prisma.$transaction([\n  prisma.user.update({ where: { id }, data: { name } }),\n  prisma.post.create({ data: { title, authorId: id } }),\n]);\n\n\u002F\u002F Interactive form — use tx client only, never the outer prisma client\nconst post = await prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  if (user.role !== 'ADMIN') throw new Error('Forbidden');\n  return tx.post.create({ data: { title, authorId: user.id } });\n});\n```\n\n### PrismaClient Singleton\n\nEach `PrismaClient` instance opens its own connection pool. Instantiate once.\n\n```ts\n\u002F\u002F lib\u002Fprisma.ts\n\n\u002F\u002F Option A — adapter-based initialization (required by newer Prisma installs)\nimport { PrismaClient } from '@prisma\u002Fclient'; \u002F\u002F or the generated client path for your setup\nimport { PrismaPg } from '@prisma\u002Fadapter-pg';\n\nfunction createPrismaClient() {\n  const adapter = new PrismaPg({\n    connectionString: process.env.DATABASE_URL!,\n  });\n  return new PrismaClient({\n    adapter,\n    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],\n  });\n}\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nexport const prisma = globalForPrisma.prisma ?? createPrismaClient();\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n\n\u002F\u002F Option B — direct initialization (older installs, no adapter needed)\n\u002F\u002F import { PrismaClient } from '@prisma\u002Fclient';\n\u002F\u002F export const prisma = globalForPrisma.prisma ?? new PrismaClient({ ... });\n```\n\nUse Option A if your Prisma install requires an `adapter` argument in the `PrismaClient` constructor.\nUse Option B if `new PrismaClient()` works without arguments. Let the compiler tell you which is correct.\n\nThe `globalThis` pattern prevents duplicate instances during hot reload (Next.js, nodemon, ts-node-dev).\n\n### N+1 Problem\n\nLoading relations inside a loop issues one query per row.\n\n```ts\n\u002F\u002F BAD: N+1 — one extra query per user\nconst users = await prisma.user.findMany();\nfor (const user of users) {\n  const posts = await prisma.post.findMany({ where: { authorId: user.id } });\n}\n\n\u002F\u002F GOOD: single query\nconst users = await prisma.user.findMany({ include: { posts: true } });\n```\n\nWith Prisma 5+ `relationJoins`, the `include` form uses a single JOIN. On large 1:N sets this may increase result set size — benchmark both approaches if the relation can return many rows per parent.\n\n## Code Examples\n\n### Cursor Pagination (preferred for feeds and large datasets)\n\n```ts\nasync function getPosts(cursor?: string, limit = 20) {\n  const items = await prisma.post.findMany({\n    where: { published: true },\n    orderBy: [\n      { createdAt: 'desc' },\n      { id: 'desc' }, \u002F\u002F secondary sort prevents unstable pagination on duplicate timestamps\n    ],\n    take: limit + 1,\n    ...(cursor && { cursor: { id: cursor }, skip: 1 }),\n  });\n\n  const hasNextPage = items.length > limit;\n  if (hasNextPage) items.pop();\n\n  return { items, nextCursor: hasNextPage ? items[items.length - 1].id : null };\n}\n```\n\nFetch `limit + 1` and pop — canonical way to detect `hasNextPage` without an extra count query. Always include a unique field (e.g. `id`) as a secondary `orderBy` to prevent unstable pagination when multiple rows share the same timestamp. Use offset pagination only when users need to jump to arbitrary pages (admin tables).\n\n### Soft Delete\n\n```ts\n\u002F\u002F Always filter explicitly — do not rely on middleware (hides behavior, hard to debug)\nconst activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });\n\nawait prisma.user.update({ where: { id }, data: { deletedAt: new Date() } });\nawait prisma.user.update({ where: { id }, data: { deletedAt: null } }); \u002F\u002F restore\n```\n\n### Error Handling\n\n```ts\nimport { Prisma } from '@prisma\u002Fclient'; \u002F\u002F or the generated client path for your setup\n\ntry {\n  await prisma.user.create({ data: { email } });\n} catch (e) {\n  if (e instanceof Prisma.PrismaClientKnownRequestError) {\n    if (e.code === 'P2002') throw new ConflictError('Email already exists');\n    if (e.code === 'P2025') throw new NotFoundError('Record not found');\n    if (e.code === 'P2003') throw new BadRequestError('Referenced record does not exist');\n  }\n  throw e;\n}\n```\n\nCommon codes: `P2002` unique violation · `P2025` not found · `P2003` foreign key violation.\n\nCatch at the service boundary and translate to domain errors. Never expose raw Prisma messages to API consumers.\n\n### Connection Pool — Serverless\n\nEmbed connection params directly in `DATABASE_URL` — string concatenation breaks if the URL already has query parameters (e.g. `?schema=public`):\n\n```bash\n# .env — preferred: embed params in the URL\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?connection_limit=1&pool_timeout=20\"\n\n# With an external pooler (PgBouncer, Supabase pooler)\nDATABASE_URL=\"postgresql:\u002F\u002Fuser:pass@host\u002Fdb?pgbouncer=true&connection_limit=1\"\n```\n\n```ts\n\u002F\u002F Vercel, AWS Lambda, and similar serverless runtimes:\n\u002F\u002F cap pool to 1 per instance; connection_limit and pool_timeout controlled via DATABASE_URL\n\n\u002F\u002F Adapter-based setup (if your Prisma install requires an adapter):\nimport { PrismaClient } from '@prisma\u002Fclient';\nimport { PrismaPg } from '@prisma\u002Fadapter-pg';\n\nconst prisma = new PrismaClient({\n  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),\n});\n\n\u002F\u002F Direct setup (if your Prisma install does not require an adapter):\n\u002F\u002F const prisma = new PrismaClient();\n```\n\n## Anti-Patterns\n\n### `updateMany` returns a count, not records\n\n```ts\n\u002F\u002F BAD: result is { count: 2 } — users[0] is undefined\nconst users = await prisma.user.updateMany({ where: { role: 'GUEST' }, data: { role: 'USER' } });\n\n\u002F\u002F GOOD: capture IDs first, then update, then fetch only the affected rows\nconst targets = await prisma.user.findMany({\n  where: { role: 'GUEST' },\n  select: { id: true },\n});\nconst ids = targets.map((u) => u.id);\nawait prisma.user.updateMany({ where: { id: { in: ids } }, data: { role: 'USER' } });\nconst updated = await prisma.user.findMany({ where: { id: { in: ids } } });\n```\n\nSame applies to `deleteMany` — returns `{ count: n }`, never the deleted rows.\n\n### `$transaction` interactive form times out after 5 seconds\n\n```ts\n\u002F\u002F BAD: external call inside transaction exceeds 5s default → \"Transaction already closed\"\nawait prisma.$transaction(async (tx) => {\n  const user = await tx.user.findUniqueOrThrow({ where: { id } });\n  await sendWelcomeEmail(user.email); \u002F\u002F external call\n  await tx.user.update({ where: { id }, data: { emailSent: true } });\n});\n\n\u002F\u002F GOOD: external calls outside the transaction\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\nawait sendWelcomeEmail(user.email);\nawait prisma.user.update({ where: { id }, data: { emailSent: true } });\n\n\u002F\u002F Only raise timeout when bulk processing genuinely needs it\nawait prisma.$transaction(async (tx) => { ... }, { timeout: 30_000 });\n```\n\n### `migrate dev` can reset the database\n\n`migrate dev` detects schema drift and may prompt to reset the DB, dropping all data.\n\n```bash\n# NEVER on shared dev, staging, or production\nnpx prisma migrate dev --name add_column\n\n# Safe everywhere except local solo dev\nnpx prisma migrate deploy\n\n# Check drift without applying\nnpx prisma migrate diff \\\n  --from-migrations .\u002Fprisma\u002Fmigrations \\\n  --to-schema-datamodel .\u002Fprisma\u002Fschema.prisma \\\n  --shadow-database-url \"$SHADOW_DATABASE_URL\"\n```\n\n### Manually editing a migration file breaks future deploys\n\nPrisma checksums every migration file. Editing after apply causes `P3006 checksum mismatch` on every environment where the original already ran. Create a new migration instead.\n\n### Breaking schema changes require multi-step migration\n\nAdding `NOT NULL` to an existing column or renaming a column in one migration will lock the table or drop data. Use expand-and-contract:\n\n```bash\n# Step 1: create migration locally, then deploy\nnpx prisma migrate dev --name add_new_column   # local only\nnpx prisma migrate deploy                       # staging \u002F production\n```\n\n```ts\n\u002F\u002F Step 2: backfill data (run in a script or migration job, not in the shell)\nawait prisma.user.updateMany({ data: { newColumn: derivedValue } });\n```\n\n```bash\n# Step 3: create the NOT NULL constraint migration locally, then deploy\nnpx prisma migrate dev --name make_new_column_required  # local only\nnpx prisma migrate deploy                               # staging \u002F production\n```\n\n### `@updatedAt` does not fire on `updateMany`\n\n`@updatedAt` is set automatically only on `update` and `upsert`. Bulk writes leave it stale.\n\n```ts\n\u002F\u002F BAD: updatedAt stays at its old value\nawait prisma.post.updateMany({ where: { authorId }, data: { published: true } });\n\n\u002F\u002F GOOD\nawait prisma.post.updateMany({\n  where: { authorId },\n  data: { published: true, updatedAt: new Date() },\n});\n```\n\n### Soft delete + `findUniqueOrThrow` leaks deleted records\n\n`findUniqueOrThrow` throws `P2025` only when the row does not exist in the DB. Soft-deleted rows still exist and are returned without error.\n\n`findUniqueOrThrow` requires a unique constraint field in `where` — adding `deletedAt: null` alongside `id` breaks the type because `{ id, deletedAt }` is not a compound unique constraint. Use `findFirstOrThrow` instead.\n\n```ts\n\u002F\u002F BAD: returns soft-deleted user\nconst user = await prisma.user.findUniqueOrThrow({ where: { id } });\n\n\u002F\u002F BAD: Prisma type error — { id, deletedAt } is not a unique constraint\nconst user = await prisma.user.findUniqueOrThrow({ where: { id, deletedAt: null } });\n\n\u002F\u002F GOOD: findFirstOrThrow supports arbitrary where conditions\nconst user = await prisma.user.findFirstOrThrow({ where: { id, deletedAt: null } });\n```\n\n### `deleteMany` without `where` deletes every row\n\n```ts\n\u002F\u002F BAD: silently wipes the table\nawait prisma.post.deleteMany();\n\n\u002F\u002F GOOD\nawait prisma.post.deleteMany({ where: { authorId: userId } });\n```\n\n## Best Practices\n\n| Rule | Reason |\n|---|---|\n| `migrate deploy` in CI\u002FCD, `migrate dev` only locally | `migrate dev` can reset the DB on drift |\n| Map entities to response DTOs | Prevents leaking internal fields |\n| Catch `PrismaClientKnownRequestError` at service boundary | Translate to domain errors |\n| Prefer `*OrThrow` methods over manual null checks | Throws P2025 automatically; use `findFirstOrThrow` when filtering non-unique fields |\n| `connection_limit=1` + external pooler in serverless | Prevents connection exhaustion |\n| Always provide `where` on `deleteMany` | Prevents accidental table wipe |\n| Set `updatedAt: new Date()` manually in `updateMany` | `@updatedAt` skips bulk writes |\n\n## Related Skills\n\n- `nestjs-patterns` — NestJS service layer that integrates Prisma\n- `postgres-patterns` — PostgreSQL-level indexing and connection tuning\n- `database-migrations` — multi-step migration planning for production\n- `backend-patterns` — general API and service layer design\n",{"name":5,"tags":49,"tools":51,"source":52,"license":14,"description":7,"source_path":53,"source_commit":54},[32,50,34,40,37],"backend",[],"https:\u002F\u002Fgithub.com\u002Faffaan-m\u002FECC","skills\u002Fprisma-patterns","bf70150eb2df8070024e5bdf08e4aa08959e2735","main","f7cd4595d0f8959822163e14f9787117765c931a","packages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fprisma-patterns",2,"https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind","https:\u002F\u002Fmonoes.github.io\u002Fmonomind\u002F","https:\u002F\u002Fgithub.com\u002Fmonoes","https:\u002F\u002Fmonoes.me","2026-11-10 01:53:48",{"source":8,"skillName":5,"commands":65,"prompt":75,"zipAvailable":76},[66,69,72],{"tool":67,"command":68},"npx","npx skills add monoes\u002Fmonomind --skill prisma-patterns",{"tool":70,"command":71},"bunx","bunx skills add monoes\u002Fmonomind --skill prisma-patterns",{"tool":73,"command":74},"pnpm","pnpm dlx skills add monoes\u002Fmonomind --skill prisma-patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fprisma-patterns\u002FSKILL.md",true,[78],{"path":79,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":82,"sourceHtmlUrl":15},"LICENSE.txt","LICENSE","许可证",1071,[84,113,145,171,195,222],{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":91,"publishedAt":112},"01M71PB3SHWKMNJM6MD65VX8D6","zk-steward","Org role guidance for a ZK steward: engineer zero-knowledge proof systems with circuit design, SNARK vs STARK choice and security review. This is zero-knowledge cryptography, not Zettelkasten note-taking.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fzk-steward\u002FSKILL.md",[],[92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111],"zk","zk steward","steward","zero-knowledge","engineer","choice","design","knowledge","impact","query","snark","review","monomind","security review","stark","zettelkasten","cryptography","security","org","blockchain","2026-11-10 13:02:02",{"skillNo":114,"name":115,"nameZh":6,"description":116,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":117,"sourceHtmlUrl":118,"categories":119,"keywords":128,"publishedAt":112},"01M71PB18SDG28WBKPKJNB814K","writing-plans","Use when you have a spec or requirements for a multi-step coding task and must write an implementation plan before touching code. Produces bite-sized TDD tasks with exact file paths, code and commands for an engineer with zero context.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwriting-plans\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwriting-plans\u002FSKILL.md",[120,123],{"categoryNo":121,"categoryName":122,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":124,"categoryName":125,"taxonomyType":126,"taxonomyTypeDesc":127},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[41,129,130,96,131,132,39,133,101,134,135,136,137,138,139,140,141,142,143,144],"writing plans","plans","superpowers","implementation plan","you","code","tasks","plan","product","write","commands","paths","suggest","coding","context","ZeRO",{"skillNo":146,"name":147,"nameZh":6,"description":148,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":149,"sourceHtmlUrl":150,"categories":151,"keywords":155,"publishedAt":112},"01M71PAXFM488X9ARXK0T04TF5","write-spec","Write a feature spec or PRD from a problem statement or feature idea. Use when turning a vague idea or user request into a structured document, scoping a feature with goals and non-goals, defining success metrics and acceptance criteria, or breaking a big ask into a phased spec.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-spec\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-spec\u002FSKILL.md",[152],{"categoryNo":153,"categoryName":154,"taxonomyType":126,"taxonomyTypeDesc":127},"01JBSEEDCAT000000000000031","产品经理",[138,156,157,158,159,99,160,39,161,162,163,137,164,165,166,167,168,169,29,170],"write spec","spec","user request","problem","acceptance","ask","vague","work","request","scoping","idea","metrics","feature","problem statement","document",{"skillNo":172,"name":173,"nameZh":6,"description":174,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":175,"sourceHtmlUrl":176,"categories":177,"keywords":181,"publishedAt":112},"01M71PASSZY800C0RPEARBS918","write-query","Write optimized SQL for your dialect with best practices. Use when translating a natural-language data need into SQL, building a multi-CTE query with joins and aggregations, optimizing a query against a large partitioned table, or getting dialect-specific syntax for Snowflake, BigQuery, Postgres, etc.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-query\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fwrite-query\u002FSKILL.md",[178],{"categoryNo":179,"categoryName":180,"taxonomyType":126,"taxonomyTypeDesc":127},"01JBSEEDCAT000000000000033","数据分析师",[101,138,182,183,184,185,99,29,39,186,187,188,189,163,190,191,192,193,194,32],"write query","postgres","for","snowflake","best practices","natural-language","with","multi","github","sql","https","syntax","plugins",{"skillNo":196,"name":197,"nameZh":6,"description":198,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":6,"sourceHtmlUrl":199,"categories":200,"keywords":207,"publishedAt":112},"01M71PAPTX0T3PF6ETFVR96SJJ","workflow-automation","Org role guidance for workflow automation: build and maintain CI\u002FCD definitions in GitHub Actions or equivalent: triggers, jobs, matrices, caching. Covers path filters, dependency caching, parallel jobs, timeouts and fail-fast ordering.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fworkflow-automation\u002FSKILL.md",[201,204],{"categoryNo":202,"categoryName":203,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":205,"categoryName":206,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[208,209,210,211,184,212,213,214,215,100,216,217,218,101,104,190,219,220,221,192],"Workflow Automation","automation","workflow","parallel","actions","ci","triggers","github actions","maintain","filters","caching","build","dependency","devops",{"skillNo":223,"name":224,"nameZh":6,"description":225,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":88,"licenseFileUrl":6,"sourceHtmlUrl":226,"categories":227,"keywords":229,"publishedAt":112},"01M71PAM19Z1XTV9NPCFSE2MY8","workflow-architect","Org role guidance for a workflow architect: map full workflow trees for systems, user journeys and agents, covering every branch and failure. Covers discovering workflows from code, handoff contracts, timeouts, recovery paths and observable state.","https:\u002F\u002Fgithub.com\u002Fmonoes\u002Fmonomind\u002Fblob\u002Fmain\u002Fpackages\u002F@monomind\u002Fcli\u002Forg-skills\u002Fworkflow-architect\u002FSKILL.md",[228],{"categoryNo":205,"categoryName":206,"taxonomyType":21,"taxonomyTypeDesc":22},[210,230,231,232,233,234,235,184,236,237,134,238,239,240,104,190,241,242,192,243],"workflow architect","architect","trees","user journeys","branch","planning","map","workflows","architecture","state","leadership","recovery","operations","systems"]