[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M736MRD911MZTCGAAEBFS2SW":3,"skill-same:01M736MRD911MZTCGAAEBFS2SW":49},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":25,"commitSha":26,"skillDirectory":27,"forksCount":28,"repositoryHtmlUrl":29,"repositoryHomepageUrl":30,"ownerHtmlUrl":31,"ownerBlogUrl":32,"ownerDescription":33,"updatedAt":34,"install":35,"resources":48},"01M736MRD911MZTCGAAEBFS2SW","prisma-patterns",null,"Prisma ORM patterns for TypeScript backends — schema design, query optimization, transactions, pagination, and critical traps like updateMany returning count not records, $transaction timeouts, migrate dev resetting the DB, @updatedAt skipped on bulk writes, and serverless connection exhaustion.","Horonomos-laps\u002FYana-AI","Horonomos-laps","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F355299122?v=4","01M76B3QPXE88ET2SQPDPVATZT"," Horonomos-laps","01M72ZQRJPCYPSSWT4D00VJR2A",13,"Apache-2.0","https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fprisma-patterns\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: prisma-patterns\ndescription: Prisma ORM patterns for TypeScript backends — schema design, query optimization, transactions, pagination, and critical traps like updateMany returning count not records, $transaction timeouts, migrate dev resetting the DB, @updatedAt skipped on bulk writes, and serverless connection exhaustion.\norigin: ECC\n---\n\n# Prisma Patterns\n\nProduction patterns and non-obvious traps for Prisma ORM in TypeScript backends.\nTested against Prisma 5.x and 6.x. Some behaviors differ from Prisma 4.\n\nCheck the Prisma version before applying version-specific patterns:\n\n```bash\nnpx prisma --version\n```\n\nPrisma 5 introduced `relationJoins`, which can load relations via JOIN rather than separate queries depending on query strategy and configuration. The `omit` field modifier and `prisma.$extends` Client Extensions API were also added. Note: `relationJoins` can cause row explosion on large 1:N relations or deep nested `include` — benchmark both approaches when relations may return many rows per parent.\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\nimport { PrismaClient } from '@prisma\u002Fclient';\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nexport const prisma =\n  globalForPrisma.prisma ??\n  new PrismaClient({\n    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],\n  });\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n```\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';\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: cap pool to 1 per instance\n\u002F\u002F connection_limit and pool_timeout are controlled via DATABASE_URL\nconst 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.\nTested against Prisma 5.x and 6.x. Some behaviors differ from Prisma 4.\n\nCheck the Prisma version before applying version-specific patterns:\n\n```bash\nnpx prisma --version\n```\n\nPrisma 5 introduced `relationJoins`, which can load relations via JOIN rather than separate queries depending on query strategy and configuration. The `omit` field modifier and `prisma.$extends` Client Extensions API were also added. Note: `relationJoins` can cause row explosion on large 1:N relations or deep nested `include` — benchmark both approaches when relations may return many rows per parent.\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\nimport { PrismaClient } from '@prisma\u002Fclient';\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nexport const prisma =\n  globalForPrisma.prisma ??\n  new PrismaClient({\n    log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],\n  });\n\nif (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;\n```\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';\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: cap pool to 1 per instance\n\u002F\u002F connection_limit and pool_timeout are controlled via DATABASE_URL\nconst 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,"origin":24,"description":7},"ECC","main","481dcd7dd2e288b9d627a2bd017db70e64e6c6fa",".claude\u002Fskills\u002Fprisma-patterns",2,"https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI","https:\u002F\u002Fyana.vutam.link\u002F","https:\u002F\u002Fgithub.com\u002FHoronomos-laps","https:\u002F\u002Fyana.vutam.link","An open-source governed runtime & execution environment for multi-agent AI systems.","2026-11-14 13:24:09",{"source":8,"skillName":5,"commands":36,"prompt":46,"zipAvailable":47},[37,40,43],{"tool":38,"command":39},"npx","npx skills add Horonomos-laps\u002FYana-AI --skill prisma-patterns",{"tool":41,"command":42},"bunx","bunx skills add Horonomos-laps\u002FYana-AI --skill prisma-patterns",{"tool":44,"command":45},"pnpm","pnpm dlx skills add Horonomos-laps\u002FYana-AI --skill prisma-patterns","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fprisma-patterns\u002FSKILL.md",true,[],[50,58,65,73,81,89],{"skillNo":51,"name":52,"nameZh":6,"description":53,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":54,"licenseFileUrl":6,"sourceHtmlUrl":55,"categories":56,"keywords":57,"publishedAt":19},"01M735GKET5YVYKAY62W2P5YXB","openai--sentry--sentry","Use when the user asks to inspect Sentry issues or events, summarize recent production errors, or pull basic Sentry health data via the Sentry API; perform read-only queries with the bundled script and require `SENTRY_AUTH_TOKEN`.","MIT","https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fopenai--sentry--sentry\u002FSKILL.md",[],[],{"skillNo":59,"name":60,"nameZh":6,"description":61,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":62,"categories":63,"keywords":64,"publishedAt":19},"01M73DCAP83T17HY7SMGK7MGFE","ito-basket-compare","Compare Itô prediction-market baskets against a user's knowledge base, portfolio notes, financial context, watchlist, or research thesis. Use for read-only basket comparison and gap analysis without investment advice or live trading.","https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fito-basket-compare\u002FSKILL.md",[],[],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":69,"sourceHtmlUrl":70,"categories":71,"keywords":72,"publishedAt":19},"01M734CYEMZFDR3KH6SW0JP8TD","implementing-network-traffic-baselining","Build network traffic baselines from NetFlow\u002FIPFIX data using Python pandas for statistical analysis, z-score anomaly detection, and hourly\u002Fdaily traffic pattern profiling","https:\u002F\u002Fgithub.com\u002Fyanacuti1121\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fimplementing-network-traffic-baselining\u002FLICENSE","https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fimplementing-network-traffic-baselining\u002FSKILL.md",[],[],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":77,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":19},"01M73ASZ6NJRJM44Z2P84NGQ3Q","detecting-evasion-techniques-in-endpoint-logs","Detects defense evasion techniques used by adversaries in endpoint logs including log tampering, timestomping, process injection, and security tool disabling. Use when investigating suspicious endpoint behavior, building detection rules for evasion tactics, or conducting threat hunting for stealthy adversary activity. Activates for requests involving evasion detection, defense evasion analysis, log tampering detection, or MITRE ATT&CK TA0005.","https:\u002F\u002Fgithub.com\u002Fyanacuti1121\u002FYana-AI\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fdetecting-evasion-techniques-in-endpoint-logs\u002FLICENSE","https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002Fcore\u002Fskills\u002Fdetecting-evasion-techniques-in-endpoint-logs\u002FSKILL.md",[],[],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":85,"sourceHtmlUrl":86,"categories":87,"keywords":88,"publishedAt":19},"01M734AFNXD06Z78QRK0VKGT7Q","implementing-microsegmentation-with-guardicore","Implementing microsegmentation using Akamai Guardicore Segmentation to map application dependencies, create granular network policies, visualize east-west traffic flows, and enforce least-privilege communication between workloads across data centers and cloud.","https:\u002F\u002Fgithub.com\u002Fyanacuti1121\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fimplementing-microsegmentation-with-guardicore\u002FLICENSE","https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fimplementing-microsegmentation-with-guardicore\u002FSKILL.md",[],[],{"skillNo":90,"name":91,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":94,"publishedAt":19},"01M736TK27QMCXK0DVCH25SJN5","ramdisk-tmpfs-patterns","https:\u002F\u002Fgithub.com\u002FHoronomos-laps\u002FYana-AI\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Framdisk-tmpfs-patterns\u002FSKILL.md",[],[]]