Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:22:25 +08:00
commit c3294f28aa
60 changed files with 10297 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
{
"name": "prisma-6",
"description": "Prisma 6 ORM patterns, client management, query optimization, and security best practices based on real-world AI coding failures",
"version": "1.0.0",
"author": {
"name": "Daniel Jankowski",
"email": "djank0113@gmail.com"
},
"skills": [
"./skills"
],
"hooks": [
"./hooks"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# prisma-6
Prisma 6 ORM patterns, client management, query optimization, and security best practices based on real-world AI coding failures

42
hooks/hooks.json Normal file
View File

@@ -0,0 +1,42 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/init-session.sh",
"timeout": 30
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/recommend-skills.sh",
"timeout": 100
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/check-prisma-client.sh",
"timeout": 30
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/check-sql-injection.sh",
"timeout": 30
},
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/check-deprecated-apis.sh",
"timeout": 30
}
]
}
]
}
}

View File

@@ -0,0 +1,7 @@
#!/bin/bash
FILE_PATH="$1"
[[ ! -f "$FILE_PATH" ]] && exit 0
grep -E "from ['\"]@prisma/client['\"]|import.*@prisma/client|\\\$queryRaw|\\\$executeRaw" "$FILE_PATH" 2>/dev/null

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLAUDE_MARKETPLACE_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
source "${CLAUDE_MARKETPLACE_ROOT}/marketplace-utils/hook-lifecycle.sh"
init_hook "prisma-6" "PreToolUse"
INPUT=$(read_hook_input)
if ! command -v grep &> /dev/null; then
log_error "grep command not found"
pretooluse_respond "allow"
finish_hook 0
fi
TS_FILES=$(find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) \
! -path "*/node_modules/*" \
! -path "*/dist/*" \
! -path "*/build/*" \
! -path "*/.next/*" 2>/dev/null || true)
if [ -z "$TS_FILES" ]; then
pretooluse_respond "allow"
finish_hook 0
fi
PRISMA_FILES=$(echo "$TS_FILES" | xargs grep -l '@prisma/client' 2>/dev/null || true)
BUFFER_USAGE=""
if [ -n "$PRISMA_FILES" ]; then
BUFFER_USAGE=$(echo "$PRISMA_FILES" | xargs grep -nE 'Buffer\.from\(' 2>/dev/null || true)
fi
if [ -n "$BUFFER_USAGE" ]; then
log_warn "Buffer.from() usage detected in files importing @prisma/client"
pretooluse_respond "allow" "Warning: Buffer.from() usage detected in files importing @prisma/client
Prisma 6 Bytes fields use Uint8Array instead of Buffer:
✗ const bytes = Buffer.from(data)
✗ reportData: Buffer.from(jsonString)
✓ reportData: new TextEncoder().encode(jsonString)
Bytes fields are returned as Uint8Array, no conversion needed:
✗ Buffer.from(user.profilePicture)
✓ user.profilePicture (already Uint8Array)"
finish_hook 0
fi
TOSTRING_ON_BYTES=$(echo "$TS_FILES" | xargs grep -nE '\.(avatar|file|attachment|document|reportData|image|photo|content|data|binary)\.toString\(' 2>/dev/null || true)
if [ -n "$TOSTRING_ON_BYTES" ]; then
log_warn ".toString() on potential Bytes fields detected"
pretooluse_respond "allow" "Warning: .toString() on potential Bytes fields detected
Bytes fields are now Uint8Array, not Buffer:
✗ reportData.toString('utf-8')
✓ new TextDecoder().decode(reportData)
For base64 encoding:
✗ avatar.toString('base64')
✓ Buffer.from(avatar).toString('base64')"
finish_hook 0
fi
NOT_FOUND_ERROR=$(echo "$TS_FILES" | xargs grep -En --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" 'Prisma.*NotFoundError|@prisma/client.*NotFoundError|PrismaClient.*NotFoundError|from.*["\047]@prisma/client["\047].*NotFoundError' 2>/dev/null || true)
if [ -n "$NOT_FOUND_ERROR" ]; then
log_warn "Deprecated NotFoundError handling detected"
pretooluse_respond "allow" "Warning: Deprecated NotFoundError handling detected
Use error code P2025 instead of NotFoundError:
✗ if (error instanceof NotFoundError)
✓ if (error.code === 'P2025')"
finish_hook 0
fi
REJECTONFOUND=$(echo "$TS_FILES" | xargs grep -n 'rejectOnNotFound' 2>/dev/null || true)
if [ -n "$REJECTONFOUND" ]; then
log_warn "Deprecated rejectOnNotFound option detected"
pretooluse_respond "allow" "Warning: Deprecated rejectOnNotFound option detected
Use findUniqueOrThrow() or findFirstOrThrow() instead:
✗ findUnique({ where: { id }, rejectOnNotFound: true })
✓ findUniqueOrThrow({ where: { id } })"
finish_hook 0
fi
EXPERIMENTAL_FEATURES=$(echo "$TS_FILES" | xargs grep -n 'experimentalFeatures.*extendedWhereUnique\|experimentalFeatures.*fullTextSearch' 2>/dev/null || true)
if [ -n "$EXPERIMENTAL_FEATURES" ]; then
log_warn "Deprecated experimental features detected"
pretooluse_respond "allow" "Warning: Deprecated experimental features detected
These features are now stable in Prisma 6:
- extendedWhereUnique (enabled by default)
- fullTextSearch (enabled by default)
Remove from schema.prisma generator block."
finish_hook 0
fi
pretooluse_respond "allow"
finish_hook 0

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLAUDE_MARKETPLACE_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
source "${CLAUDE_MARKETPLACE_ROOT}/marketplace-utils/hook-lifecycle.sh"
init_hook "prisma-6" "PreToolUse"
INPUT=$(read_hook_input)
if ! command -v grep &> /dev/null; then
log_error "grep command not found"
pretooluse_respond "allow"
finish_hook 0
fi
TS_FILES=$(find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) \
! -path "*/node_modules/*" \
! -path "*/dist/*" \
! -path "*/build/*" \
! -path "*/.next/*" 2>/dev/null || true)
if [ -z "$TS_FILES" ]; then
pretooluse_respond "allow"
finish_hook 0
fi
INSTANCES=$(echo "$TS_FILES" | xargs grep -n --exclude="*.test.ts" --exclude="*.spec.ts" --exclude-dir="__tests__" --exclude-dir="test" "new PrismaClient()" 2>/dev/null || true)
if [ -n "$INSTANCES" ]; then
INSTANCE_COUNT=$(echo "$INSTANCES" | wc -l | tr -d ' ')
if [ "$INSTANCE_COUNT" -gt 1 ]; then
log_warn "Multiple PrismaClient instances detected: $INSTANCE_COUNT"
pretooluse_respond "allow" "Warning: Multiple PrismaClient instances detected ($INSTANCE_COUNT)
Use global singleton pattern to prevent connection pool exhaustion:
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma"
finish_hook 0
fi
fi
FUNCTION_SCOPED=$(echo "$TS_FILES" | xargs grep -B5 --exclude="*.test.ts" --exclude="*.spec.ts" --exclude-dir="__tests__" --exclude-dir="test" "new PrismaClient()" 2>/dev/null | \
grep -E "(function|const.*=.*\(|async.*\()" || true)
if [ -n "$FUNCTION_SCOPED" ]; then
IS_SINGLETON_WRAPPER=false
for file in $TS_FILES; do
if grep -q "new PrismaClient()" "$file" 2>/dev/null; then
HAS_SINGLETON_FUNC=$(grep -E "const.*singleton.*=.*\(\).*=>.*new PrismaClient\(\)" "$file" || true)
HAS_GLOBAL_CACHE=$(grep -E "globalThis|globalForPrisma" "$file" || true)
SINGLETON_CALLS=$(grep -o "singleton()" "$file" 2>/dev/null | wc -l | tr -d ' ')
if [ -n "$HAS_SINGLETON_FUNC" ] && [ -n "$HAS_GLOBAL_CACHE" ] && [ "$SINGLETON_CALLS" -le 1 ]; then
IS_SINGLETON_WRAPPER=true
break
fi
fi
done
if [ "$IS_SINGLETON_WRAPPER" = false ]; then
log_warn "PrismaClient instantiated inside function scope"
pretooluse_respond "allow" "Warning: PrismaClient instantiated inside function scope
This creates new instances on each function call, exhausting connections.
Move PrismaClient to module scope with singleton pattern."
finish_hook 0
fi
fi
RAW_INTERPOLATION=$(echo "$TS_FILES" | xargs grep -n "prisma\.\(raw\|queryRaw\|executeRaw\)" 2>/dev/null | \
grep -E '\$\{.*\}|"\s*\+\s*[^+]|\`\$\{' || true)
if [ -n "$RAW_INTERPOLATION" ]; then
log_warn "Potential SQL interpolation in Prisma.raw() call"
pretooluse_respond "allow" "Warning: Potential SQL interpolation detected in Prisma.raw() call
Use parameterized queries to prevent SQL injection:
prisma.\$queryRaw\`SELECT * FROM users WHERE id = \${id}\`
Avoid string concatenation:
❌ prisma.\$queryRaw('SELECT * FROM users WHERE id = ' + id)
❌ prisma.\$queryRaw(\`SELECT * FROM users WHERE id = \${unsafeVar}\`)"
finish_hook 0
fi
MISSING_GLOBAL=$(echo "$TS_FILES" | xargs grep -L "globalForPrisma\|globalThis.*prisma" 2>/dev/null | \
xargs grep -l "new PrismaClient()" 2>/dev/null || true)
if [ -n "$MISSING_GLOBAL" ]; then
log_warn "PrismaClient instantiation without global singleton pattern"
pretooluse_respond "allow" "Warning: PrismaClient instantiation without global singleton pattern
Recommended pattern for Next.js and serverless environments:
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()"
finish_hook 0
fi
pretooluse_respond "allow"
finish_hook 0

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLAUDE_MARKETPLACE_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
source "${CLAUDE_MARKETPLACE_ROOT}/marketplace-utils/hook-lifecycle.sh"
init_hook "prisma-6" "PreToolUse"
INPUT=$(read_hook_input)
if ! command -v grep &> /dev/null; then
log_error "grep command not found"
pretooluse_respond "allow"
finish_hook 0
fi
TS_FILES=$(find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) \
! -path "*/node_modules/*" \
! -path "*/dist/*" \
! -path "*/build/*" \
! -path "*/.next/*" 2>/dev/null || true)
if [ -z "$TS_FILES" ]; then
pretooluse_respond "allow"
finish_hook 0
fi
UNSAFE_QUERY_RAW=$(echo "$TS_FILES" | xargs grep -n --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" '\$queryRawUnsafe' 2>/dev/null || true)
if [ -n "$UNSAFE_QUERY_RAW" ]; then
log_error "Unsafe raw SQL query detected - SQL injection risk"
pretooluse_respond "block" "Warning: Unsafe raw SQL query detected - SQL injection risk
Use \$queryRaw with tagged template syntax instead:
✗ prisma.\$queryRawUnsafe(\`SELECT * FROM User WHERE id = \${id}\`)
✓ prisma.\$queryRaw\`SELECT * FROM User WHERE id = \${id}\`"
finish_hook 0
fi
RAW_WITH_INTERPOLATION=$(echo "$TS_FILES" | xargs grep -n --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" 'Prisma\.raw(' 2>/dev/null | \
grep -E '\$\{|\+.*["\`]' || true)
if [ -n "$RAW_WITH_INTERPOLATION" ]; then
log_error "Prisma.raw() with string interpolation - SQL injection risk"
pretooluse_respond "block" "Warning: Prisma.raw() with string interpolation - SQL injection risk
Use Prisma.sql with tagged template syntax:
✗ Prisma.raw(\`WHERE id = \${id}\`)
✓ Prisma.sql\`WHERE id = \${id}\`"
finish_hook 0
fi
MISSING_TAGGED_TEMPLATE=$(echo "$TS_FILES" | xargs grep -n --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" '\$queryRaw(' 2>/dev/null || true)
if [ -n "$MISSING_TAGGED_TEMPLATE" ]; then
log_warn "\$queryRaw with function call syntax instead of tagged template"
pretooluse_respond "allow" "Warning: \$queryRaw with function call syntax instead of tagged template
Use tagged template syntax for automatic parameterization:
✗ prisma.\$queryRaw(Prisma.sql\`...\`)
✓ prisma.\$queryRaw\`...\`"
finish_hook 0
fi
EXECUTE_RAW_UNSAFE=$(echo "$TS_FILES" | xargs grep -n --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" '\$executeRawUnsafe' 2>/dev/null || true)
if [ -n "$EXECUTE_RAW_UNSAFE" ]; then
log_error "Unsafe raw SQL execution detected - SQL injection risk"
pretooluse_respond "block" "Warning: Unsafe raw SQL execution detected - SQL injection risk
Use \$executeRaw with tagged template syntax instead:
✗ prisma.\$executeRawUnsafe(\`UPDATE User SET name = '\${name}'\`)
✓ prisma.\$executeRaw\`UPDATE User SET name = \${name}\`"
finish_hook 0
fi
pretooluse_respond "allow"
finish_hook 0

14
hooks/scripts/init-session.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLAUDE_MARKETPLACE_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
source "${CLAUDE_MARKETPLACE_ROOT}/marketplace-utils/hook-lifecycle.sh"
init_hook "prisma-6" "SessionStart"
log_info "Prisma 6 session initialized"
inject_context "Prisma 6 plugin session started"
finish_hook 0

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLAUDE_MARKETPLACE_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
source "${CLAUDE_MARKETPLACE_ROOT}/marketplace-utils/hook-lifecycle.sh"
init_hook "prisma-6" "PostToolUse"
INPUT=$(read_hook_input)
FILE_PATH=$(get_input_field "tool_input.file_path")
if [[ -z "$FILE_PATH" ]]; then
FILE_PATH=$(get_input_field "tool_input.path")
fi
if [[ -z "$FILE_PATH" ]]; then
finish_hook 0
fi
FILE_NAME="${FILE_PATH##*/}"
FILE_DIR="${FILE_PATH%/*}"
RECOMMENDATION_TYPE=""
SKILLS=""
MESSAGE=""
if [[ "$FILE_NAME" == "schema.prisma" ]]; then
RECOMMENDATION_TYPE="schema_files"
SKILLS="MIGRATIONS-*, CLIENT-*, QUERIES-type-safety"
MESSAGE="Prisma Schema: $SKILLS"
elif [[ "$FILE_DIR" == *"migrations"* ]]; then
RECOMMENDATION_TYPE="migration_files"
SKILLS="MIGRATIONS-dev-workflow, MIGRATIONS-production, MIGRATIONS-v6-upgrade"
MESSAGE="Prisma Migrations: $SKILLS"
elif [[ "$FILE_PATH" =~ \.(ts|js|tsx|jsx)$ ]]; then
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$SCRIPT_DIR")")}"
IMPORTS=$(bash "$PLUGIN_ROOT/hooks/scripts/analyze-imports.sh" "$FILE_PATH" 2>/dev/null || true)
if [[ "$IMPORTS" == *"@prisma/client"* ]]; then
RECOMMENDATION_TYPE="prisma_files"
SKILLS="CLIENT-*, QUERIES-*, TRANSACTIONS-*, SECURITY-*"
MESSAGE="Prisma Client Usage: $SKILLS"
if [[ "$IMPORTS" == *"\$queryRaw"* ]]; then
RECOMMENDATION_TYPE="raw_sql_context"
SKILLS="SECURITY-sql-injection (CRITICAL)"
MESSAGE="Raw SQL Detected: $SKILLS"
fi
fi
if [[ "$FILE_PATH" == *"vercel"* || "$FILE_PATH" == *"lambda"* || "$FILE_PATH" == *"app/"* ]]; then
if ! has_shown_recommendation "prisma-6" "serverless_context"; then
log_info "Recommending skills: CLIENT-serverless-config, PERFORMANCE-connection-pooling for serverless context in $FILE_PATH"
mark_recommendation_shown "prisma-6" "serverless_context"
inject_context "Serverless Context: CLIENT-serverless-config, PERFORMANCE-connection-pooling"
fi
fi
fi
if [[ -z "$RECOMMENDATION_TYPE" ]]; then
finish_hook 0
fi
if ! has_shown_recommendation "prisma-6" "$RECOMMENDATION_TYPE"; then
log_info "Recommending skills: $SKILLS for $FILE_PATH"
mark_recommendation_shown "prisma-6" "$RECOMMENDATION_TYPE"
inject_context "$MESSAGE
Use Skill tool to activate specific skills when needed."
fi
finish_hook 0

269
plugin.lock.json Normal file
View File

@@ -0,0 +1,269 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:djankies/claude-configs:prisma-6",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "4f2cdb280481d3c92cb348a79537a94fa2400065",
"treeHash": "e48183c1fbf4eb9a3d6204cf1aec88ced0362d568d029682017611f426c3e4eb",
"generatedAt": "2025-11-28T10:16:30.577181Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "prisma-6",
"description": "Prisma 6 ORM patterns, client management, query optimization, and security best practices based on real-world AI coding failures",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "e743bda8fb8c277a91dfaa78056fcbc7546aa8db5fd92992fc4be7e440128384"
},
{
"path": "hooks/hooks.json",
"sha256": "c0e174ea912b5a6b3eadaa39f2fb8bfeacab23ce3795640631d0bfc1c9a5cab2"
},
{
"path": "hooks/scripts/init-session.sh",
"sha256": "f6a761031ee8eacea0f678c1a984fd0a6501498f6b99be8e10c19d8d38b05279"
},
{
"path": "hooks/scripts/check-deprecated-apis.sh",
"sha256": "fd9d858b8958f526a2b594f66942937731817c4bf53fab5fc702ecd00671acb4"
},
{
"path": "hooks/scripts/check-sql-injection.sh",
"sha256": "94ab1b68d9534f0435f520e98280afb78d62855f88e3855352a253e2920c6b3a"
},
{
"path": "hooks/scripts/recommend-skills.sh",
"sha256": "b1568ea05fd125a6efc81dd163fe83049311f25bf87f9d35d78038bb33ed4fd7"
},
{
"path": "hooks/scripts/analyze-imports.sh",
"sha256": "7e84051262adf6ed6c65b6280cdc2bf7eecee01ce2d6e4f5e276d679f8bb99ba"
},
{
"path": "hooks/scripts/check-prisma-client.sh",
"sha256": "7fd4365eaae75fedf79b4583f2d836ee4e3cce71bba4dffbbbcda7b520d2d10f"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "7134fd4581a3bcac31fca0bb352cd71052a6afb71433df2e09b9fedf4035f192"
},
{
"path": "skills/configuring-transaction-isolation/SKILL.md",
"sha256": "179a1ccdb18fcd092f80e506aa131c3aa836d69a2d6eefde132227ca2773fb85"
},
{
"path": "skills/configuring-transaction-isolation/references/race-conditions.md",
"sha256": "c732b17d3f01fd1433ebb403ed003b91307e232da057d5141d996fbeceaaf044"
},
{
"path": "skills/configuring-transaction-isolation/references/complete-examples.md",
"sha256": "9f0c9f8563de2888cb9ad7243b8543b04a5d37f866ac18f92ce89b51f440bda3"
},
{
"path": "skills/configuring-transaction-isolation/references/database-defaults.md",
"sha256": "b96cd7fb9cdf40c0b57d6ad3978705d7aa025e8b18e93dffe26b5a035b835b2d"
},
{
"path": "skills/managing-client-lifecycle/SKILL.md",
"sha256": "838da7b11ff6cd3d0b6c750857caa0b0f75d842b237b3e445dcc056e97a8dabf"
},
{
"path": "skills/upgrading-to-prisma-6/SKILL.md",
"sha256": "3321f1ce60f684c931ab71a9dfb7d69492b39e178420b429fc63b1a13f0f5f09"
},
{
"path": "skills/upgrading-to-prisma-6/references/troubleshooting.md",
"sha256": "57201ba9167ebd0926ce94c94319c22f7478ed66a7846ceb3ecd5e4a727ae519"
},
{
"path": "skills/upgrading-to-prisma-6/references/breaking-changes.md",
"sha256": "b0477ac93d07e3e7c3099653e6d188877950c6bbc84f67d5a44ca38ae375b2ca"
},
{
"path": "skills/upgrading-to-prisma-6/references/migration-examples.md",
"sha256": "b05ce34031f4c9ef328d965d8a49529612ee991c6b90dd7412e37747d8fc82db"
},
{
"path": "skills/upgrading-to-prisma-6/references/migration-checklist.md",
"sha256": "044f5c9ed22d5aec23b7ba280c053dda96c6cfdaa192f559f8aee037a917555a"
},
{
"path": "skills/handling-transaction-errors/SKILL.md",
"sha256": "830d139c95b289a836ef9d5b9ef2b8c1f4741b4ca9b3bdf29cf9078a66be9a13"
},
{
"path": "skills/managing-dev-migrations/SKILL.md",
"sha256": "58f5bcb6d5f626f705fabcb2c0519ee66a87764c0a0d28c33b04f89be77ac013"
},
{
"path": "skills/preventing-sql-injection/SKILL.md",
"sha256": "47a740c2797fd65fc92c218789f1c516eb7512dc44c3a4f175d6e84dfc1c5f69"
},
{
"path": "skills/configuring-serverless-clients/SKILL.md",
"sha256": "fa738a9805387ba70e394bf2c39a3ddb236c4eed3ad99dd20739a0fc79c05572"
},
{
"path": "skills/deploying-production-migrations/SKILL.md",
"sha256": "bc1bf68adc2b37264f8c2dc694c89003d896fd6a5872a27ce3843627808afb4d"
},
{
"path": "skills/creating-client-singletons/SKILL.md",
"sha256": "de072702ab7fa00095f4c4d526e12ff8b3f95fd977f55eb9e5173da0b201e79e"
},
{
"path": "skills/creating-client-singletons/references/common-scenarios.md",
"sha256": "5ca4170ddce253e3301978ae6230e005d67c8b3214704b4f945f5ee012b10558"
},
{
"path": "skills/creating-client-singletons/references/serverless-pattern.md",
"sha256": "edf43aba55f173916a6cbf058ee63bc4f4d99561664ddc95d9a7d2ff5a0f2a80"
},
{
"path": "skills/creating-client-singletons/references/test-pattern.md",
"sha256": "3a167bce3e2f6e48348a5cb52d2156650eafeec2c87add62921b805a35561cab"
},
{
"path": "skills/implementing-query-caching/SKILL.md",
"sha256": "acb75dc228e4b442a9dbc075ca2c3a3d0932604a27110575eaabc43bd0f2bc88"
},
{
"path": "skills/implementing-query-caching/references/common-pitfalls.md",
"sha256": "c809b0c56aae176648395c06898f4b887298c4dce51aaac241cb0281e1b2ac72"
},
{
"path": "skills/implementing-query-caching/references/redis-configuration.md",
"sha256": "4a89a2b74b5055cafb522a24cca20db52ca0ed59ec17da72000f7eb70eb4d795"
},
{
"path": "skills/implementing-query-caching/references/invalidation-patterns.md",
"sha256": "fb8ddf27e47db48061d2bb4171613f9f5fedfb9d124a29bed26c22de9086cdbf"
},
{
"path": "skills/implementing-query-caching/references/advanced-examples.md",
"sha256": "e669eaeeee792f452f75325b55d2c6e6e1e26ea293d934b3c46bc5701641adb9"
},
{
"path": "skills/optimizing-query-selection/SKILL.md",
"sha256": "ac7d6725e9738281434e226b31a72b9e964880304ae1d2ded37fac7133bdb1b1"
},
{
"path": "skills/optimizing-query-selection/references/performance-verification.md",
"sha256": "c0ee06e7f4ae961324a69074fb61bb82942376c5824910f519233b9a60124fe4"
},
{
"path": "skills/optimizing-query-selection/references/n-plus-one-prevention.md",
"sha256": "acb5b8ebaedbb9fd0a0711b8c82cea9fe17000ebe3560c90c8576b1e35a9a095"
},
{
"path": "skills/optimizing-query-selection/references/nested-selection.md",
"sha256": "45c43fed9e61a7c8bb5204911893627b9ca31b02e63ba02b8dd19479ff00fc1e"
},
{
"path": "skills/optimizing-query-selection/references/api-optimization.md",
"sha256": "ad4f22d35b63c73fc4015b6bb915c5171ca4e6220b794ee659f89fcd415d36e9"
},
{
"path": "skills/optimizing-query-selection/references/type-safety.md",
"sha256": "11d4979627f6e3d94c4cad12276e0bf0354bfb9c0d53c7797caf7eb572b4caa0"
},
{
"path": "skills/ensuring-query-type-safety/SKILL.md",
"sha256": "ea6fa0e0e97b27569767c4c79f5f34f4ea06e09ba89f295d86dd06db8621a7a1"
},
{
"path": "skills/implementing-query-pagination/SKILL.md",
"sha256": "49a8e5d8306db7609e41f4dcac7845d8d603d7d1c18904d428aa190318d7c953"
},
{
"path": "skills/implementing-query-pagination/references/bidirectional-pagination.md",
"sha256": "6e91573db591b802e39373d98c11d424711722f2c454d4605eefe16867ca3004"
},
{
"path": "skills/implementing-query-pagination/references/performance-comparison.md",
"sha256": "5cc004c1d01dff9e6e44bfc0d96ebe75c57742129a6377bb91906b21fe9a587c"
},
{
"path": "skills/implementing-query-pagination/references/data-change-handling.md",
"sha256": "16395d4b6ea0a4520fbdfd642c8dd2e77d7ee2cb31c0d931585ef69121ffee21"
},
{
"path": "skills/implementing-query-pagination/references/api-implementation-examples.md",
"sha256": "dcd25744008846d34d6f2611148a35e1d95b5010343d497f4f9b2571a14ae7ab"
},
{
"path": "skills/implementing-query-pagination/references/common-mistakes.md",
"sha256": "79fbd120831921bb5c644371ed3776463d5ce8e74bd16e480c84c47196c1d41d"
},
{
"path": "skills/optimizing-query-performance/SKILL.md",
"sha256": "8db4b17aa4db272430867932dd34988de4983d0b72a255e50e16bec5853a17c0"
},
{
"path": "skills/optimizing-query-performance/references/optimization-examples.md",
"sha256": "ffdfa4390ec8fb60829427d4df0ead8344806bd1f6a2618280b1a207be60bce0"
},
{
"path": "skills/optimizing-query-performance/references/batch-operations.md",
"sha256": "ecb54278c48052a5e690d9c8fa0b98107113e4760b1e62323245dae84c08cc6b"
},
{
"path": "skills/optimizing-query-performance/references/query-monitoring.md",
"sha256": "912afcba9a801ebd7e3a46321e8f39ac8bc2b20cdecd86e6ca866b99c82526be"
},
{
"path": "skills/optimizing-query-performance/references/field-selection.md",
"sha256": "707b310ca100252bb5ed97583d08257a310d15a3fb287c4af852e1228d1417dd"
},
{
"path": "skills/optimizing-query-performance/references/index-strategy.md",
"sha256": "f61aa5b9118dc81826996288ffc9d01bb9337b03e5adfd02cc30936d6ac4f743"
},
{
"path": "skills/using-interactive-transactions/SKILL.md",
"sha256": "f5bd5fb2d9c831a5bdb431b9c4d749af34a96805ff3ed80af4c246e97a6e2e28"
},
{
"path": "skills/reviewing-prisma-patterns/SKILL.md",
"sha256": "3332d02e79b15eac4c453fefcaaa023c2b3877840cbb1ad4d5a9f76646251ea7"
},
{
"path": "skills/reviewing-prisma-patterns/references/example-reviews.md",
"sha256": "62d9c3c560242d4179d81901b8e0dd3ce9c342f1a224a1cc28da686a075c34b4"
},
{
"path": "skills/reviewing-prisma-patterns/references/validation-checks.md",
"sha256": "51746bce5a437f48815f5be8d61d1ee465459111aab06cd5b3b85fc9e9a86e3a"
},
{
"path": "skills/validating-query-inputs/SKILL.md",
"sha256": "d7c66c69cf34e412acd15601bbff4315afaecad4a4ac38d98d50407793c2188b"
},
{
"path": "skills/preventing-error-exposure/SKILL.md",
"sha256": "9f8ac2d7a20f3f3229ff4fb41e7331805331ad89e5be0b4dd7237b972e65626c"
},
{
"path": "skills/configuring-connection-pools/SKILL.md",
"sha256": "b9be0e08c84d72b734576530fb7dd425f4e418694a17b8b95736aaca182bc295"
}
],
"dirSha256": "e48183c1fbf4eb9a3d6204cf1aec88ced0362d568d029682017611f426c3e4eb"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,159 @@
---
name: configuring-connection-pools
description: Configure connection pool sizing for optimal performance. Use when configuring DATABASE_URL or deploying to production.
allowed-tools: Read, Write, Edit
---
# Connection Pooling Performance
Configure Prisma Client connection pools for optimal performance and resource utilization.
## Pool Sizing Formula
**Standard environments:** `connection_limit = (num_cpus × 2) + 1`
Examples: 4 CPU → 9, 8 CPU → 17, 16 CPU → 33 connections
**Configure in DATABASE_URL:**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=9&pool_timeout=20"
```
**Configure in schema.prisma:**
```prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
previewFeatures = ["metrics"]
}
```
## Serverless Environments
**Always use `connection_limit=1` per instance**: Serverless platforms scale horizontally; total connections = instances × limit. Example: 100 Lambda instances × 1 = 100 DB connections (safe) vs. 100 × 10 = 1,000 (exhausted).
**AWS Lambda / Vercel / Netlify:**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=0&connect_timeout=10"
```
**Additional optimizations:**
- `pool_timeout=0`: Fail fast instead of waiting for connections
- `connect_timeout=10`: Timeout initial DB connection
- `pgbouncer=true`: Use PgBouncer transaction mode
## PgBouncer for High Concurrency
**Deploy external pooler when:** >100 application instances, unpredictable serverless scaling, multiple apps sharing one database, connection exhaustion, frequent P1017 errors
**Configuration:**
```ini
[databases]
mydb = host=postgres.internal port=5432 dbname=production
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5
reserve_pool_timeout = 3
```
\*\*Prisma with
PgBouncer:\*\*
```
DATABASE_URL="postgresql://user:pass@pgbouncer:6432/db?pgbouncer=true&connection_limit=10"
```
**Avoid in transaction mode:** Prepared statements (disabled with `pgbouncer=true`), persistent SET variables, LISTEN/NOTIFY, advisory locks, temporary tables
## Bottleneck Identification
**P1017 Error (Connection pool timeout)**: "Can't reach database server at `localhost:5432`"
Causes: connection_limit too low, slow queries holding connections, missing cleanup, database at max_connections limit
**Diagnosis:**
```typescript
import { Prisma } from '@prisma/client';
const prisma = new Prisma.PrismaClient({
log: [{ emit: 'event', level: 'query' }],
});
prisma.$on('query', (e) => {
console.log('Query duration:', e.duration);
});
const metrics = await prisma.$metrics.json();
console.log('Pool metrics:', metrics);
```
**Check pool status:**
```sql
SELECT count(*) as connections, state, wait_event_type, wait_event
FROM pg_stat_activity WHERE datname = 'your_database'
GROUP BY state, wait_event_type, wait_event;
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity;
```
## Pool Configuration Parameters
| Parameter | Standard | Serverless | PgBouncer | Notes |
| ------------------ | ---------------- | ------------- | --------- | ---------------------------------------------------- |
| `connection_limit` | num_cpus × 2 + 1 | 1 | 1020 | Total connections = instances × limit |
| `pool_timeout` | 2030 sec | 0 (fail fast) | — | Wait time for available connection |
| `connect_timeout` | 5 sec | 10 sec | — | Initial connection timeout; 1530 for network issues |
**Complete URL example:**
```
postgresql://user:pass@host:5432/db?connection_limit=9&pool_timeout=20&connect_timeout=10&socket_timeout=0&statement_cache_size=100
```
## Production Deployment Checklist
- [ ] Calculate `connection_limit` based on CPU/instance count
- [ ] Set `pool_timeout` appropriately for environment
- [ ] Enable query logging to identify
slow queries
- [ ] Monitor P1017 errors
- [ ] Set up database connection monitoring
- [ ] Configure PgBouncer if serverless/high concurrency
- [ ] Load test with realistic connection counts
- [ ] Document pool settings in runbook
**Environment-specific settings:**
| Environment | URL Pattern |
| ------------------------- | ----------------------------------------------------------------------------- |
| Traditional servers | `postgresql://user:pass@host:5432/db?connection_limit=17&pool_timeout=20` |
| Containers with PgBouncer | `postgresql://user:pass@pgbouncer:6432/db?pgbouncer=true&connection_limit=10` |
| Serverless functions | `postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=0` |
## Common Mistakes
**Default limit in serverless**: Each Lambda instance uses ~10 connections, exhausting DB with 50+ concurrent functions. **Fix:** `connection_limit=1`
**High pool_timeout in serverless**: Functions wait 30s for connections, hitting timeout. **Fix:** `pool_timeout=0`
**No PgBouncer with high concurrency**: 200+ application instances with direct connections = exhaustion. **Fix:** Deploy PgBouncer with transaction pooling.
**Connection_limit exceeds database max_connections**: Setting `connection_limit=200` when DB max is 100. **Fix:** Use PgBouncer or reduce limit below database maximum.

View File

@@ -0,0 +1,163 @@
---
name: configuring-serverless-clients
description: Configure PrismaClient for serverless (Next.js, Lambda, Vercel) with connection_limit=1 and global singleton pattern.
allowed-tools: Read, Write, Edit, Glob, Grep
---
# Serverless PrismaClient Configuration
Configure PrismaClient for serverless platforms to prevent connection pool exhaustion using connection limits and global singleton patterns.
## Activation Triggers
Deploy to Next.js (App/Pages Router), AWS Lambda, Vercel, or similar serverless platforms; encountering P1017 errors; files in app/, pages/api/, lambda/ directories.
## Problem & Solution
**Problem:** Each Lambda instance creates its own connection pool. Default unlimited pool × instances exhausts database (e.g., 10 instances × 10 connections = 100 connections).
**Solution:** Set `connection_limit=1` in DATABASE_URL; use global singleton to reuse client across invocations; consider PgBouncer for high concurrency.
## Configuration Workflow
**Phase 1—Environment:** Add `connection_limit=1` and `pool_timeout=20` to DATABASE_URL in .env; configure in Vercel dashboard if applicable.
**Phase 2—Client Singleton:** Create single global PrismaClient in `lib/prisma.ts`; use `globalThis` pattern (Next.js 13+ App Router), `global.prisma` pattern (Pages Router/Lambda), or initialize outside handler (Lambda standalone).
**Phase 3—Validation:** Verify DATABASE_URL contains connection parameters; confirm `new PrismaClient()` appears only in `lib/prisma.ts`; test with 10+ concurrent requests; monitor connection count.
## Implementation Patterns
### Next.js App Router (`lib/prisma.ts`)
```typescript
import { PrismaClient } from '@prisma/client';
const prismaClientSingleton = () => new PrismaClient();
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
export default prisma;
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma;
```
**Environment:** `DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=20"`
**Usage in Server Action:**
```typescript
import prisma from '@/lib/prisma';
export async function createUser(formData: FormData) {
'use server';
return await prisma.user.create({
data: {
email: formData.get('email') as string,
name: formData.get('name') as string,
},
});
}
```
### Next.js Pages Router / AWS Lambda (`lib/prisma.ts`)
```typescript
import { PrismaClient } from '@prisma/client';
declare global {
var prisma: PrismaClient | undefined;
}
const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') global.prisma = prisma;
export default prisma;
```
**Lambda Handler:**
```typescript
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const handler = async (event: any) => {
const users = await prisma.user.findMany();
return { statusCode: 200, body: JSON.stringify(users) };
// Never call prisma.$disconnect() reuse Lambda container connections
};
```
### With PgBouncer (High Concurrency)
Use when >50
concurrent requests or connection limits still cause issues.
```bash
DATABASE_URL="postgresql://user:pass@pgbouncer-host:6543/db?connection_limit=10&pool_timeout=20"
DIRECT_URL="postgresql://user:pass@direct-host:5432/db"
```
```prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
```
DATABASE_URL → PgBouncer (queries); DIRECT_URL → database (migrations).
## Requirements
**MUST:** Set `connection_limit=1` in DATABASE_URL; use global singleton (never `new PrismaClient()` in functions); create single `lib/prisma.ts` imported throughout; add `pool_timeout`.
**SHOULD:** Use PgBouncer for high concurrency (>50); monitor production connection count; set limit via URL parameter; reuse Lambda connections.
**NEVER:** Create `new PrismaClient()` in routes/handlers; use default pool size in serverless; call `$disconnect()` in handlers; deploy without limits; split instances across files.
## Common Mistakes
| Issue | Wrong | Right |
| ------------------------------ | --------------------------------------------------------- | ------------------------------------------ |
| Connection in Constructor | `new PrismaClient({ datasources: { db: { url: ... } } })` | Set `connection_limit=1` in DATABASE_URL |
| Multiple Instances | `const prisma = new PrismaClient()` inside functions | Import singleton from `lib/prisma` |
| Handler Disconnect | `await prisma.$disconnect()` after query | Remove disconnect reuse containers |
| Missing TypeScript Declaration | `const prisma = global.prisma \|\| ...` | `declare global { var prisma: ... }` first |
## Validation Checklist
1. **Environment:** DATABASE_URL contains `connection_limit=1` and `pool_timeout`; verified in Vercel dashboard.
2. **Code:** `new PrismaClient()` appears exactly once in `lib/prisma.ts`; all other files import from it.
3. **Testing:** 10+ concurrent requests to staging; connection count stays ≤10-15; no P1017 errors.
4. **Monitoring:** Production alerts for connection exhaustion and timeout errors.
## Platform-Specific Notes
**Vercel:** Set environment variables in dashboard (
auto-encrypted); connection pooling shared across invocations; consider Vercel Postgres for built-in pooling.
**AWS Lambda:** Container reuse varies by traffic; cold starts create new instances; use provisioned concurrency for consistency; Lambda layers optimize Prisma binary size.
**Cloudflare Workers:** Standard PrismaClient unsupported (V8 isolates, not Node.js); use Data Proxy or D1.
**Railway/Render:** Apply `connection_limit=1` pattern; check platform docs for built-in pooling.
## Related Skills
**Next.js Integration:**
- If implementing authenticated data access layers in Next.js, use the securing-data-access-layer skill from nextjs-16 for verifySession() DAL patterns
- If securing server actions with database operations, use the securing-server-actions skill from nextjs-16 for authentication patterns
## References
- Next.js patterns: `references/nextjs-patterns.md`
- Lambda optimization: `references/lambda-patterns.md`
- PgBouncer setup: `references/pgbouncer-guide.md`

View File

@@ -0,0 +1,347 @@
---
name: configuring-transaction-isolation
description: Configure transaction isolation levels to prevent race conditions and handle concurrent access. Use when dealing with concurrent updates, financial operations, inventory management, or when users mention race conditions, dirty reads, phantom reads, or concurrent modifications.
allowed-tools: Read, Write, Edit
version: 1.0.0
---
# Transaction Isolation Levels
This skill teaches how to configure transaction isolation levels in Prisma to prevent race conditions and handle concurrent database access correctly.
---
<role>
This skill teaches Claude how to configure and use transaction isolation levels in Prisma 6 to prevent concurrency issues like race conditions, dirty reads, phantom reads, and lost updates.
</role>
<when-to-activate>
This skill activates when:
- User mentions race conditions, concurrent updates, or dirty reads
- Working with financial transactions, inventory systems, or booking platforms
- Implementing operations that must maintain consistency under concurrent access
- User asks about Serializable, RepeatableRead, or ReadCommitted isolation
- Dealing with P2034 errors (transaction conflicts)
</when-to-activate>
<overview>
Transaction isolation levels control how database transactions interact with each other when running concurrently. Prisma supports setting isolation levels to prevent common concurrency issues.
**Key Isolation Levels:**
1. **Serializable** - Strictest isolation, prevents all anomalies
2. **RepeatableRead** - Prevents dirty and non-repeatable reads
3. **ReadCommitted** - Prevents dirty reads only (default for most databases)
4. **ReadUncommitted** - No isolation (not recommended)
**Common Concurrency Issues:**
- **Dirty Reads:** Reading uncommitted changes from other transactions
- **Non-Repeatable Reads:** Same query returns different results within transaction
- **Phantom Reads:** New rows appear in repeated queries
- **Lost Updates:** Concurrent updates overwrite each other
**When to Set Isolation:**
- Financial operations (payments, transfers, refunds)
- Inventory management (stock reservations, order fulfillment)
- Booking systems (seat reservations, room bookings)
- Any operation requiring strict consistency
</overview>
<workflow>
## Standard Workflow
**Phase 1: Identify Concurrency Risk**
1. Analyze operation for concurrent access patterns
2. Determine what consistency guarantees are needed
3. Choose appropriate isolation level based on requirements
**Phase 2: Configure Isolation Level**
1. Set isolation level in transaction options
2. Implement proper error handling for conflicts
3. Add retry logic if appropriate
**Phase 3: Handle Isolation Conflicts**
1. Catch P2034 errors (transaction conflicts)
2. Retry with exponential backoff if appropriate
3. Return clear error messages to users
</workflow>
<isolation-level-guide>
## Isolation Level Quick Reference
| Level | Prevents | Use Cases | Trade-offs |
|-------|----------|-----------|------------|
| **Serializable** | All anomalies | Financial transactions, critical inventory | Highest consistency, lowest concurrency, more P2034 errors |
| **RepeatableRead** | Dirty reads, non-repeatable reads | Reports, multi-step reads | Good balance, still allows phantom reads |
| **ReadCommitted** | Dirty reads only | Standard operations, high-concurrency | Highest concurrency, allows non-repeatable/phantom reads |
| **ReadUncommitted** | Nothing | Not recommended | Almost never appropriate |
### Serializable Example
```typescript
await prisma.$transaction(
async (tx) => {
const account = await tx.account.findUnique({
where: { id: accountId }
});
if (account.balance < amount) {
throw new Error('Insufficient funds');
}
await tx.account.update({
where: { id: accountId },
data: { balance: { decrement: amount } }
});
await tx.transaction.create({
data: {
accountId,
amount: -amount,
type: 'WITHDRAWAL'
}
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
}
);
```
### RepeatableRead Example
```typescript
await prisma.$transaction(
async (tx) => {
const user = await tx.user.findUnique({
where: { id: userId },
include: { orders: true }
});
const totalSpent = user.orders.reduce(
(sum, order) => sum + order.amount,
0
);
await tx.user.update({
where: { id: userId },
data: {
tierLevel: calculateTier(totalSpent),
lastCalculatedAt: new Date()
}
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead
}
);
```
### ReadCommitted Example
```typescript
await prisma.$transaction(
async (tx) => {
await tx.log.create({
data: {
level: 'INFO',
message: 'User logged in',
userId
}
});
await tx.user.update({
where: { id: userId },
data: { lastLoginAt: new Date() }
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted
}
);
```
</isolation-level-guide>
<decision-tree>
## Choosing Isolation Level
Follow this decision tree:
**Is this a financial operation (money, payments, credits)?**
- YES → Use `Serializable`
- NO → Continue
**Does the operation read data multiple times and require it to stay constant?**
- YES → Use `RepeatableRead`
- NO → Continue
**Is this a high-concurrency operation where conflicts are expensive?**
- YES → Use `ReadCommitted` (or no explicit isolation)
- NO → Continue
**Does the operation modify data based on a read within the transaction?**
- YES → Use `RepeatableRead` minimum
- NO → Use `ReadCommitted` (or no explicit isolation)
**Still unsure?**
- Start with `RepeatableRead` for safety
- Monitor P2034 error rate
- Adjust based on actual concurrency patterns
</decision-tree>
<error-handling>
## Handling Isolation Conflicts
### P2034: Transaction Conflict
When using Serializable isolation, conflicts are common under concurrency:
```typescript
async function transferWithRetry(
fromId: string,
toId: string,
amount: number,
maxRetries = 3
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
await prisma.$transaction(
async (tx) => {
const fromAccount = await tx.account.findUnique({
where: { id: fromId }
});
if (fromAccount.balance < amount) {
throw new Error('Insufficient funds');
}
await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } }
});
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } }
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 5000,
timeout: 10000
}
);
return { success: true };
} catch (error) {
if (error.code === 'P2034' && attempt < maxRetries - 1) {
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 100)
);
continue;
}
throw error;
}
}
throw new Error('Transaction failed after max retries');
}
```
**Key Elements:**
- Retry loop with attempt counter
- Check for P2034 error code
- Exponential backoff between retries
- maxWait and timeout configuration
- Final error if all retries exhausted
### Timeout Configuration
```typescript
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 5000,
timeout: 10000
}
```
- `maxWait`: Maximum time to wait for transaction to start (milliseconds)
- `timeout`: Maximum time for transaction to complete (milliseconds)
Higher isolation levels need higher timeouts to handle conflicts.
</error-handling>
<constraints>
## Constraints and Guidelines
**MUST:**
- Use Serializable for financial operations
- Handle P2034 errors explicitly
- Set appropriate maxWait and timeout values
- Validate data before starting transaction
- Use atomic operations (increment/decrement) when possible
**SHOULD:**
- Implement retry logic with exponential backoff for Serializable
- Keep transactions as short as possible
- Read all data needed before making decisions
- Log isolation conflicts for monitoring
- Consider RepeatableRead before defaulting to Serializable
**NEVER:**
- Use ReadUncommitted in production
- Ignore P2034 errors
- Retry indefinitely without limit
- Mix isolation levels in same operation
- Assume isolation level is higher than default without setting it
</constraints>
<validation>
## Validation
After implementing isolation levels:
1. **Concurrency Testing:**
- Simulate concurrent requests to same resource
- Verify no lost updates or race conditions occur
- Expected: Conflicts detected and handled gracefully
2. **Performance Monitoring:**
- Monitor P2034 error rate
- Track transaction retry attempts
- If P2034 > 5%: Consider lowering isolation level or optimizing transaction duration
3. **Error Handling:**
- Verify P2034 errors return user-friendly messages
- Check retry logic executes correctly
- Ensure transactions eventually succeed or fail definitively
</validation>
---
## References
For additional details and advanced scenarios, see:
- [Database-Specific Defaults](./references/database-defaults.md) - PostgreSQL, MySQL, SQLite, MongoDB isolation behaviors
- [Race Condition Patterns](./references/race-conditions.md) - Lost updates, double-booking, phantom reads
- [Complete Examples](./references/complete-examples.md) - Banking transfers, inventory reservations, seat bookings

View File

@@ -0,0 +1,222 @@
# Complete Examples
## Example 1: Banking Transfer
**Input:** Transfer money between accounts with strict consistency.
**Implementation:**
```typescript
import { Prisma } from '@prisma/client';
async function transferMoney(
fromAccountId: string,
toAccountId: string,
amount: number
) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
try {
const result = await prisma.$transaction(
async (tx) => {
const fromAccount = await tx.account.findUnique({
where: { id: fromAccountId }
});
if (!fromAccount) {
throw new Error('Source account not found');
}
if (fromAccount.balance < amount) {
throw new Error('Insufficient funds');
}
const toAccount = await tx.account.findUnique({
where: { id: toAccountId }
});
if (!toAccount) {
throw new Error('Destination account not found');
}
await tx.account.update({
where: { id: fromAccountId },
data: { balance: { decrement: amount } }
});
await tx.account.update({
where: { id: toAccountId },
data: { balance: { increment: amount } }
});
const transfer = await tx.transfer.create({
data: {
fromAccountId,
toAccountId,
amount,
status: 'COMPLETED',
completedAt: new Date()
}
});
return transfer;
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 5000,
timeout: 10000
}
);
return { success: true, transfer: result };
} catch (error) {
if (error.code === 'P2034') {
throw new Error('Transaction conflict - please retry');
}
throw error;
}
}
```
## Example 2: Inventory Reservation
**Input:** Reserve inventory items for an order.
**Implementation:**
```typescript
async function reserveInventory(
orderId: string,
items: Array<{ productId: string; quantity: number }>
) {
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
await prisma.$transaction(
async (tx) => {
for (const item of items) {
const product = await tx.product.findUnique({
where: { id: item.productId }
});
if (!product) {
throw new Error(`Product ${item.productId} not found`);
}
if (product.stock < item.quantity) {
throw new Error(
`Insufficient stock for ${product.name}`
);
}
await tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
});
await tx.reservation.create({
data: {
orderId,
productId: item.productId,
quantity: item.quantity,
reservedAt: new Date()
}
});
}
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
maxWait: 3000,
timeout: 8000
}
);
return { success: true };
} catch (error) {
if (error.code === 'P2034' && attempt < maxRetries - 1) {
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 200)
);
continue;
}
throw error;
}
}
throw new Error('Reservation failed after retries');
}
```
## Example 3: Seat Booking with Status Check
**Input:** Book a seat with concurrent user protection.
**Implementation:**
```typescript
async function bookSeat(
userId: string,
eventId: string,
seatNumber: string
) {
try {
const booking = await prisma.$transaction(
async (tx) => {
const seat = await tx.seat.findFirst({
where: {
eventId,
seatNumber
}
});
if (!seat) {
throw new Error('Seat not found');
}
if (seat.status !== 'AVAILABLE') {
throw new Error('Seat is no longer available');
}
await tx.seat.update({
where: { id: seat.id },
data: {
status: 'BOOKED',
bookedAt: new Date()
}
});
const booking = await tx.booking.create({
data: {
userId,
seatId: seat.id,
eventId,
status: 'CONFIRMED',
bookedAt: new Date()
}
});
return booking;
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
}
);
return { success: true, booking };
} catch (error) {
if (error.code === 'P2034') {
throw new Error(
'Seat was just booked by another user - please select another seat'
);
}
throw error;
}
}
```

View File

@@ -0,0 +1,63 @@
# Database-Specific Defaults
## PostgreSQL
Default: `ReadCommitted`
Supported levels:
- `Serializable` (strictest)
- `RepeatableRead`
- `ReadCommitted` (default)
Notes:
- PostgreSQL uses true Serializable isolation (not snapshot isolation)
- May throw serialization errors under high concurrency
- Excellent MVCC implementation reduces conflicts
## MySQL
Default: `RepeatableRead`
Supported levels:
- `Serializable`
- `RepeatableRead` (default)
- `ReadCommitted`
- `ReadUncommitted` (not recommended)
Notes:
- InnoDB engine required for transaction support
- Uses gap locking in RepeatableRead mode
- Serializable adds locking to SELECT statements
## SQLite
Default: `Serializable`
Supported levels:
- `Serializable` (only level - database-wide lock)
Notes:
- Only one writer at a time
- No true isolation level configuration
- Best for single-user or low-concurrency applications
## MongoDB
Default: `Snapshot` (similar to RepeatableRead)
Supported levels:
- `Snapshot` (equivalent to RepeatableRead)
- `Majority` read concern
Notes:
- Different isolation model than SQL databases
- Uses write-ahead log for consistency
- Replica set required for transactions

View File

@@ -0,0 +1,124 @@
# Preventing Race Conditions
## Lost Update Problem
**Scenario:** Two transactions read the same value, both update it, one overwrites the other.
**Without Isolation:**
```typescript
const product = await prisma.product.findUnique({
where: { id: productId }
});
await prisma.product.update({
where: { id: productId },
data: { stock: product.stock - quantity }
});
```
Transaction A reads stock: 10
Transaction B reads stock: 10
Transaction A writes stock: 5 (10 - 5)
Transaction B writes stock: 8 (10 - 2)
Result: Stock is 8, but should be 3
**With Serializable Isolation:**
```typescript
await prisma.$transaction(
async (tx) => {
const product = await tx.product.findUnique({
where: { id: productId }
});
if (product.stock < quantity) {
throw new Error('Insufficient stock');
}
await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } }
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
}
);
```
One transaction succeeds, the other gets P2034 and retries with fresh data.
## Double-Booking Problem
**Scenario:** Two users try to book the same resource simultaneously.
**Solution:**
```typescript
async function bookSeat(userId: string, seatId: string) {
try {
await prisma.$transaction(
async (tx) => {
const seat = await tx.seat.findUnique({
where: { id: seatId }
});
if (seat.status !== 'AVAILABLE') {
throw new Error('Seat no longer available');
}
await tx.seat.update({
where: { id: seatId },
data: {
status: 'BOOKED',
userId,
bookedAt: new Date()
}
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
}
);
return { success: true };
} catch (error) {
if (error.code === 'P2034') {
throw new Error('Seat was just booked by another user');
}
throw error;
}
}
```
## Phantom Read Problem
**Scenario:** Query for rows matching a condition, insert happens, re-query shows different results.
**Example with RepeatableRead:**
```typescript
await prisma.$transaction(
async (tx) => {
const activeUsers = await tx.user.findMany({
where: { status: 'ACTIVE' }
});
const count = activeUsers.length;
await tx.report.create({
data: {
type: 'USER_COUNT',
value: count,
timestamp: new Date()
}
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead
}
);
```
RepeatableRead prevents other transactions from changing existing rows, but may still allow new inserts (phantom reads) depending on database implementation.

View File

@@ -0,0 +1,173 @@
---
name: creating-client-singletons
description: Prevent multiple PrismaClient instances that exhaust connection pools causing P1017 errors. Use when creating PrismaClient, exporting database clients, setting up Prisma in new files, or encountering connection pool errors. Critical for serverless environments.
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
version: 1.0.0
---
# PrismaClient Singleton Pattern
Teaches global singleton pattern to prevent multiple PrismaClient instances from exhausting database connection pools.
---
**Role:** Teach proper PrismaClient instantiation and export via global singleton pattern to prevent connection pool exhaustion, P1017 errors, and serverless deployment failures.
**When to Activate:** Creating PrismaClient instances, setting up database clients/exports, encountering P1017 errors, working with serverless environments (Next.js, Lambda, Vercel), or reviewing @prisma/client code.
---
## Problem & Solution
**Problem:** Creating multiple `new PrismaClient()` instances (the #1 Prisma violation) creates separate connection pools causing: pool exhaustion/P1017 errors, performance degradation, serverless failures (Lambda instances × pool size = disaster), and memory waste. Critical: 80% of AI agents in testing created multiple instances causing production failures.
**Solution:** Use global singleton pattern with module-level export: (1) check if PrismaClient exists globally, (2) create if none exists, (3) export for module reuse, (4) never instantiate in functions/classes. Supports module-level singletons (Node.js), global singletons (serverless/hot-reload), test patterns, and pool configuration.
---
## Implementation Workflow
**Phase 1 — Assess:** Grep for `@prisma/client` imports and `new PrismaClient()` calls; identify environment type (hot-reload vs. serverless vs. traditional Node.js vs. tests).
**Phase 2 — Implement:** Choose pattern based on environment; create/update client export (`lib/db.ts` or `lib/prisma.ts`) using global singleton check; update all imports to use singleton; remove duplicate instantiations.
**Phase 3 — Validate:** Grep for `new PrismaClient()` (
should appear once only); test hot reload; verify no P1017 errors; check database connection count; monitor production deployment and logs.
---
## Examples
### Module-Level Singleton (Traditional Node.js)
**File: `lib/db.ts`**
```typescript
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;
```
**Usage:** `import prisma from '@/lib/db'` — works because module loads once in Node.js, creating single shared instance.
---
### Global Singleton (Next.js/Hot Reload)
**File: `lib/prisma.ts`**
```typescript
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
```
**Why:** `globalThis` survives hot module reload; development reuses client across reloads; production creates clean instance per deployment; prevents "too many clients" during development.
---
### Anti-Pattern: Function-Scoped Creation
**WRONG:**
```typescript
async function getUsers() {
const prisma = new PrismaClient(); // ❌ New pool every call
const users = await prisma.user.findMany();
await prisma.$disconnect();
return users;
}
```
**Problems:** New connection pool per function call; connection overhead kills performance; pool never warms up; exhausts connections under load.
**Fix:** `import prisma from '@/lib/db'` and use it directly without creating new instances.
---
## Reference Files
- **Serverless Pattern:** `references/serverless-pattern.md` — Next.js App Router, Vercel, AWS Lambda configurations
- **Test Pattern:** `references/test-pattern.md` — test setup, mocking, isolation strategies
- **Common Scenarios:** `references/common-scenarios.md` — codebase conversion, P1017 troubleshooting, configuration
Load when working with serverless, writing tests, or troubleshooting specific issues.
---
## Constraints
**MUST:** Create PrismaClient exactly
once; export from centralized module (`lib/db.ts`); use global singleton in hot-reload environments; import singleton in all database-access files; never instantiate inside functions or classes.
**SHOULD:** Place in `lib/db.ts`, `lib/prisma.ts`, or `src/db.ts`; configure logging based on NODE_ENV; set connection pool size for deployment; use TypeScript; document connection configuration.
**NEVER:** Create PrismaClient in route handlers, API endpoints, service functions, test files, utility functions; create multiple instances "just to be safe"; disconnect/reconnect repeatedly.
---
## Validation
After implementing:
| Check | Command/Method | Expected | Issue |
| ------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------- |
| Multiple instances | `grep -r "new PrismaClient()" --include="*.ts" --include="*.js"` | Exactly one occurrence (singleton file only) | Consolidate to single singleton |
| Import patterns | `grep -r "from '@prisma/client'"` | Most imports from singleton module; only singleton imports from `@prisma/client` | Update imports to use singleton |
| Connection pool | Monitor during development hot reload | Connection count stays constant (not growing) | Global singleton pattern not working |
| Production errors | Check logs for P1017 | Zero connection pool errors | Check serverless connection_limit config |
| Test isolation | Run test suite | Tests pass; no connection errors | Ensure tests import singleton |
---
## Standard Client Export
**TypeScript:**
```typescript
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
```
**JavaScript:**
```javascript
const { PrismaClient } = require('@prisma/client');
const globalForPrisma = globalThis;
const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
module.exports = prisma;
```
---
## Quick Reference
**Setup Checklist:**
- [ ] Create `lib/db.ts` or `lib/prisma.ts`; use global singleton pattern (hot reload environments); export single instance; configure logging by NODE_ENV; set connection_limit for serverless; import singleton in all files; never create PrismaClient elsewhere; validate with grep (one instance); test hot reload; monitor production connections
**Red Flags (Implement Singleton Immediately):**
- Multiple `new PrismaClient()` in grep results; P1017 errors in logs; growing connection count during development; different files importing from `@prisma/client`; PrismaClient creation inside functions; test files creating own clients
## Related Skills
**TypeScript Type Safety:**
- If using type guards for singleton validation, use the using-type-guards skill from typescript for type narrowing patterns

View File

@@ -0,0 +1,310 @@
# Common Scenarios
Real-world scenarios and solutions for PrismaClient singleton pattern.
## Scenario 1: Converting Existing Codebase
**Current state:** Multiple files create their own PrismaClient
**Steps:**
1. Create central singleton: `lib/db.ts`
```typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
2. Use Grep to find all `new PrismaClient()` calls:
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" .
```
3. Replace with imports from `lib/db.ts`:
**Before:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function getUsers() {
return await prisma.user.findMany()
}
```
**After:**
```typescript
import { prisma } from '@/lib/db'
export async function getUsers() {
return await prisma.user.findMany()
}
```
4. Remove old instantiations
5. Validate with grep (should find only one instance):
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" . | wc -l
```
Expected: `1`
---
## Scenario 2: Next.js Application
**Setup:**
1. Create `lib/prisma.ts` with global singleton pattern
2. Import in Server Components:
```typescript
import { prisma } from '@/lib/prisma'
export default async function UsersPage() {
const users = await prisma.user.findMany()
return <UserList users={users} />
}
```
3. Import in Server Actions:
```typescript
'use server'
import { prisma } from '@/lib/prisma'
export async function createUser(formData: FormData) {
const email = formData.get('email') as string
return await prisma.user.create({ data: { email } })
}
```
4. Import in Route Handlers:
```typescript
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}
```
5. Set `connection_limit=1` in DATABASE_URL for Vercel:
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1"
```
**Validation:**
- Hot reload shouldn't create new connections
- No P1017 errors in development
- Production deployments handle concurrent requests
---
## Scenario 3: Encountering P1017 Errors
**Symptoms:**
- "Can't reach database server" errors
- "Too many connections" in database logs
- Intermittent connection failures
- Error code: P1017
**Diagnosis:**
1. Grep codebase for `new PrismaClient()`:
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" .
```
2. Check count of instances:
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" . | wc -l
```
If > 1: Multiple instance problem
3. Review connection pool configuration:
```bash
grep -rn "connection_limit" .env* schema.prisma
```
If missing in serverless: Misconfiguration problem
**Fix:**
1. Implement singleton pattern (see Scenario 1)
2. Configure connection_limit for serverless:
**Development (.env.local):**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10"
```
**Production (Vercel):**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1"
```
3. Monitor connection count after deployment:
```sql
SELECT count(*) FROM pg_stat_activity WHERE datname = 'your_database';
```
Expected: Should stabilize at reasonable number (not growing)
---
## Scenario 4: Multiple Files Creating Clients
**Problem:** Different service files create their own clients
**Before:**
**`services/users.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function getUsers() {
return await prisma.user.findMany()
}
```
**`services/posts.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function getPosts() {
return await prisma.post.findMany()
}
```
**Problems:**
- Two separate connection pools
- Doubled memory usage
- Doubled connection count
- Multiplies with every service file
**After:**
**`lib/db.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prisma
```
**`services/users.ts`:**
```typescript
import prisma from '@/lib/db'
export async function getUsers() {
return await prisma.user.findMany()
}
```
**`services/posts.ts`:**
```typescript
import prisma from '@/lib/db'
export async function getPosts() {
return await prisma.post.findMany()
}
```
**Result:**
- Single connection pool shared across services
- Reduced memory usage
- Stable connection count
---
## Connection Pool Configuration
The singleton pattern works with proper pool configuration:
**Default pool size:** 10 connections per PrismaClient
**Serverless (Vercel, Lambda):**
```
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=1"
```
**Traditional servers:**
Calculate: `connection_limit = (num_instances * 2) + 1`
- 1 server = 3 connections
- 2 servers = 5 connections
- 4 servers = 9 connections
**Development:**
Default (10) is fine since only one developer instance runs.
**Example configuration per environment:**
```typescript
const connectionLimit = process.env.NODE_ENV === 'production'
? 1
: 10
export const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL + `?connection_limit=${connectionLimit}`
}
}
})
```
---
## Why This Matters
Real-world impact from stress testing:
- **80% of agents** created multiple instances
- **100% of those** would fail in production under load
- **P1017 errors** in serverless after ~10 concurrent requests
- **Memory leaks** from abandoned connection pools
- **Database locked out** teams during testing
**The singleton pattern prevents all of these issues.**
Use this pattern **always**, even if your app is small. It becomes critical as you scale, and retrofitting is painful.

View File

@@ -0,0 +1,238 @@
# Serverless Pattern
Serverless environments require special handling due to cold starts, connection pooling, and function lifecycle constraints.
## Next.js App Router (Vercel)
**File: `lib/prisma.ts`**
```typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
**Environment Configuration (.env):**
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10"
```
**Why connection_limit=1:**
- Each serverless function instance gets ONE connection
- Multiple function instances = multiple connections
- Prevents pool exhaustion with many concurrent requests
- Vercel scales to hundreds of instances automatically
**Usage in Server Components:**
```typescript
import { prisma } from '@/lib/prisma'
export default async function UsersPage() {
const users = await prisma.user.findMany()
return <UserList users={users} />
}
```
**Usage in Server Actions:**
```typescript
'use server'
import { prisma } from '@/lib/prisma'
export async function createUser(formData: FormData) {
const email = formData.get('email') as string
return await prisma.user.create({
data: { email }
})
}
```
**Usage in Route Handlers:**
```typescript
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}
```
**Key Points:**
- Never create PrismaClient in component files
- Import singleton from `lib/prisma.ts`
- Global pattern survives hot reload
- Connection limit prevents pool exhaustion
---
## AWS Lambda
**File: `lib/db.ts`**
```typescript
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
if (!global.prisma) {
global.prisma = new PrismaClient({
log: ['error', 'warn']
})
}
prisma = global.prisma
export default prisma
```
**Lambda Handler:**
```typescript
import prisma from './lib/db'
export async function handler(event: any) {
const users = await prisma.user.findMany()
return {
statusCode: 200,
body: JSON.stringify(users)
}
}
```
**Environment Variables (Lambda):**
```
DATABASE_URL=postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10&connect_timeout=10
```
**Lambda-Specific Considerations:**
- Lambda reuses container for warm starts
- Global singleton survives across invocations
- First invocation creates client (cold start)
- Subsequent invocations reuse client (warm starts)
- No need to disconnect (Lambda freezes container)
---
## Connection Pool Calculation for Serverless
**Formula:**
```
max_connections = (max_concurrent_functions * connection_limit) + buffer
```
**Example (Vercel):**
- Max concurrent functions: 100
- Connection limit per function: 1
- Buffer: 10
**Result:** Need 110 database connections
**Recommended DATABASE_URL for Vercel:**
```
postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10
```
**Why pool_timeout=10:**
- Prevents long waits for connections
- Fails fast if pool exhausted
- User gets error instead of timeout
---
## Anti-Pattern: Multiple Files Creating Clients
**WRONG - Each file creates its own:**
**`app/api/users/route.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET() {
return Response.json(await prisma.user.findMany())
}
```
**`app/api/posts/route.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET() {
return Response.json(await prisma.post.findMany())
}
```
**Problems in Serverless:**
- Each route = separate client = separate pool
- 2 routes × 50 function instances × 10 connections = 1000 connections!
- Database exhausted under load
- P1017 errors inevitable
**Fix - Central singleton:**
**`lib/prisma.ts`:**
```typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
**`app/api/users/route.ts`:**
```typescript
import { prisma } from '@/lib/prisma'
export async function GET() {
return Response.json(await prisma.user.findMany())
}
```
**`app/api/posts/route.ts`:**
```typescript
import { prisma } from '@/lib/prisma'
export async function GET() {
return Response.json(await prisma.post.findMany())
}
```
**Result:**
- 50 function instances × 1 connection = 50 connections
- Sustainable and scalable
- No P1017 errors

View File

@@ -0,0 +1,259 @@
# Test Pattern
Proper test setup with PrismaClient singleton ensures test isolation and prevents connection exhaustion.
## Test File Setup
**Import singleton, don't create:**
```typescript
import { prisma } from '@/lib/prisma'
describe('User operations', () => {
beforeEach(async () => {
await prisma.user.deleteMany()
})
it('creates user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
afterAll(async () => {
await prisma.$disconnect()
})
})
```
**Key Points:**
- Import singleton, don't create
- Clean state with `deleteMany` or transactions
- Disconnect once at end of suite
- Don't disconnect between tests (kills connection pool)
---
## Test Isolation with Transactions
**Better approach for test isolation:**
```typescript
import { prisma } from '@/lib/prisma'
import { PrismaClient } from '@prisma/client'
describe('User operations', () => {
let testPrisma: Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>
beforeEach(async () => {
await prisma.$transaction(async (tx) => {
testPrisma = tx
await tx.user.deleteMany()
})
})
it('creates user', async () => {
const user = await testPrisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
})
```
**Why this works:**
- Each test runs in transaction
- Automatic rollback after test
- No data leakage between tests
- Faster than deleteMany
---
## Mocking PrismaClient for Unit Tests
**When to mock:**
- Testing business logic without database
- Fast unit tests
- CI/CD pipeline optimization
**File: `__mocks__/prisma.ts`**
```typescript
import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
export const prismaMock = mockDeep<PrismaClient>()
beforeEach(() => {
mockReset(prismaMock)
})
```
**File: `__tests__/userService.test.ts`**
```typescript
import { prismaMock } from '../__mocks__/prisma'
import { createUser } from '../services/userService'
jest.mock('@/lib/prisma', () => ({
__esModule: true,
default: prismaMock,
}))
describe('User Service', () => {
it('creates user with email', async () => {
const mockUser = { id: '1', email: 'test@example.com' }
prismaMock.user.create.mockResolvedValue(mockUser)
const user = await createUser('test@example.com')
expect(user.email).toBe('test@example.com')
expect(prismaMock.user.create).toHaveBeenCalledWith({
data: { email: 'test@example.com' }
})
})
})
```
**Key Points:**
- Mock the singleton module, not PrismaClient
- Reset mocks between tests
- Type-safe mocks with jest-mock-extended
- Fast tests without database
---
## Integration Test Setup
**File: `tests/setup.ts`**
```typescript
import { prisma } from '@/lib/prisma'
beforeAll(async () => {
await prisma.$connect()
})
afterAll(async () => {
await prisma.$disconnect()
})
export async function cleanDatabase() {
const tables = ['User', 'Post', 'Comment']
for (const table of tables) {
await prisma[table.toLowerCase()].deleteMany()
}
}
```
**File: `tests/users.integration.test.ts`**
```typescript
import { prisma } from '@/lib/prisma'
import { cleanDatabase } from './setup'
describe('User Integration Tests', () => {
beforeEach(async () => {
await cleanDatabase()
})
it('creates and retrieves user', async () => {
const created = await prisma.user.create({
data: { email: 'test@example.com' }
})
const retrieved = await prisma.user.findUnique({
where: { id: created.id }
})
expect(retrieved?.email).toBe('test@example.com')
})
it('handles unique constraint', async () => {
await prisma.user.create({
data: { email: 'test@example.com' }
})
await expect(
prisma.user.create({
data: { email: 'test@example.com' }
})
).rejects.toThrow(/Unique constraint/)
})
})
```
**Key Points:**
- Shared setup in `tests/setup.ts`
- Clean database between tests
- Test real database behavior
- Catch constraint violations
---
## Anti-Pattern: Creating Client in Tests
**WRONG:**
```typescript
import { PrismaClient } from '@prisma/client'
describe('User tests', () => {
let prisma: PrismaClient
beforeEach(() => {
prisma = new PrismaClient()
})
afterEach(async () => {
await prisma.$disconnect()
})
it('creates user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
})
```
**Problems:**
- New connection pool every test
- Connect/disconnect overhead
- Connection exhaustion in large suites
- Slow tests
**Fix:**
```typescript
import { prisma } from '@/lib/prisma'
describe('User tests', () => {
beforeEach(async () => {
await prisma.user.deleteMany()
})
it('creates user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com' }
})
expect(user.email).toBe('test@example.com')
})
})
```
**Result:**
- Reuses singleton connection
- Fast tests
- No connection exhaustion

View File

@@ -0,0 +1,167 @@
---
name: deploying-production-migrations
description: Deploy migrations to production safely using migrate deploy in CI/CD. Use when setting up production deployment pipelines.
allowed-tools: Read, Write, Edit, Bash
---
# MIGRATIONS-production
## Overview
Production database migrations require careful orchestration to prevent data loss and downtime. This covers safe migration deployment using `prisma migrate deploy` in CI/CD pipelines, failure handling, and rollback strategies.
## Production Migration Commands
### Safe Command
**prisma migrate deploy**: Applies pending migrations only; records history in `_prisma_migrations`; neither creates migrations nor resets database.
```bash
npx prisma migrate deploy
```
### Prohibited Commands
**prisma migrate dev**: Creates migrations, can reset database (development-only)
**prisma migrate reset**: Drops/recreates database, deletes all data
**prisma db push**: Bypasses migration history, no rollback capability, risks data loss
## CI/CD Integration
```yaml
# GitHub Actions
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx prisma generate
- run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- run: npm run deploy
```
```yaml
# GitLab CI
deploy-production:
stage: deploy
image: node:20
only: [main]
environment:
name: production
before_script:
- npm ci && npx prisma generate
script:
- npx prisma migrate deploy
- npm run deploy
variables:
DATABASE_URL: $DATABASE_URL_PRODUCTION
```
```dockerfile
# Docker
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY prisma ./prisma
RUN npx prisma generate
COPY . .
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]
```
## Handling Failed Migrations
\*\*Check status
\*\*: `npx prisma migrate status` (identifies pending, applied, failed migrations, and schema drift)
**Resolution options**:
- **Temporary failure**: `npx prisma migrate resolve --applied <name> && npx prisma migrate deploy`
- **Partially reverted**: `npx prisma migrate resolve --rolled-back <name>`
- **Buggy migration**: Create new migration to fix: `npx prisma migrate dev --name fix_previous_migration`, then deploy
**Manual rollback**: Create down migration (SQL to revert changes), apply via `npx prisma migrate dev --name rollback_* --create-only`
## Production Deployment Checklist
**Pre-Deployment**: All migrations tested in staging; backup created; rollback plan documented; downtime window scheduled; team notified
**Deployment**: Maintenance mode enabled (if needed); run `npx prisma migrate deploy`; verify status; run smoke tests; monitor logs
**Post-Deployment**: Verify all migrations applied; check functionality; monitor database performance; disable maintenance mode; document issues
## Database Connection Best Practices
**Connection pooling**: `DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=20"`
**Security**: Never commit DATABASE_URL; use environment variables, CI/CD secrets, or secret management tools (Vault, AWS Secrets Manager)
**Read replicas**: Separate migration connection from app connections; migrations always target primary database:
```env
DATABASE_URL="postgresql://primary:5432/db"
DATABASE_URL_REPLICA="postgresql://replica:5432/db"
```
## Zero-Downtime Migrations
**Expand-Contract Pattern**:
1. **Expand** (add new column, keep old): `ALTER TABLE "User" ADD COLUMN "email_new" TEXT;` Deploy app writing to both.
2. **Migrate data**: `UPDATE "User" SET "email_new" = "email"
WHERE "email_new" IS NULL;`3. **Contract** (remove old):`ALTER TABLE "User" DROP COLUMN "email"; ALTER TABLE "User" RENAME COLUMN "email_new" TO "email";`
**Backwards-compatible**: Make columns optional first, enforce constraints in later migration.
## Monitoring and Alerts
**Duration tracking**: `time npx prisma migrate deploy`; set alerts for migrations exceeding expected duration
**Failure alerts**:
```yaml
- run: npx prisma migrate deploy
- if: failure()
run: curl -X POST $SLACK_WEBHOOK -d '{"text":"Production migration failed!"}'
```
**Schema drift detection**: `npx prisma migrate status` fails if schema differs from migrations
## Common Production Issues
| Issue | Cause | Solution |
| ----------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| Migration hangs | Long-running query, table locks | Identify blocking queries; run during low-traffic window; use `SET statement_timeout = '30s';` in PostgreSQL |
| Migration fails midway | Constraint violation, data type mismatch | Check migration status; mark as applied if data correct; create fix migration if needed |
| Out-of-order migrations | Multiple developers creating migrations simultaneously | Merge conflicts in migration files; regenerate if needed; enforce linear history |
## Configuration
**Shadow Database** (Prisma 6): Not needed for `migrate deploy`, only `migrate dev`
```env
DATABASE_URL="postgresql://..."
SHADOW_DATABASE_URL="postgresql://...shadow"
```
**Multi-Environment Strategy**:
- Development: `npx prisma migrate dev`
- Staging: `npx prisma migrate deploy` (test production process)
- Production: `npx prisma migrate deploy` (apply only, never create)
## References
[Prisma Migrate Deploy Documentation](https://www.prisma.io/docs/orm/prisma-migrate/workflows/deploy) | [Production Best Practices](https://www.prisma.io/docs/orm/prisma-migrate/workflows/production) | [Troubleshooting Migrations](https://www.prisma.io/docs/orm/prisma-migrate/workflows/troubleshooting)

View File

@@ -0,0 +1,288 @@
---
name: ensuring-query-type-safety
description: Use Prisma's generated types, `Prisma.validator`, and `GetPayload` for type-safe queries.
allowed-tools: Read, Write, Edit
---
# Type-Safe Queries in Prisma 6
Ensure type safety in all database queries using Prisma's generated types, `Prisma.validator` for custom fragments, and `GetPayload` for type inference.
## Generated Types
Prisma automatically generates TypeScript types from your schema.
**Basic Usage:**
```typescript
import { Prisma, User } from '@prisma/client';
async function getUser(id: string): Promise<User | null> {
return prisma.user.findUnique({ where: { id } });
}
```
**With Relations:**
```typescript
type UserWithPosts = Prisma.UserGetPayload<{ include: { posts: true } }>;
async function getUserWithPosts(id: string): Promise<UserWithPosts | null> {
return prisma.user.findUnique({
where: { id },
include: { posts: true },
});
}
```
## Prisma.validator for Custom Types
Use `Prisma.validator` to create reusable, type-safe query fragments.
```typescript
// Query validator
const userWithProfile = Prisma.validator<Prisma.UserDefaultArgs>()({
include: {
profile: true,
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
},
},
});
type UserWithProfile = Prisma.UserGetPayload<typeof userWithProfile>;
async function getCompleteUser(id: string): Promise<UserWithProfile | null> {
return prisma.user.findUnique({
where: { id },
...userWithProfile,
});
}
// Input validator
const createUserInput = Prisma.validator<Prisma.UserCreateInput>()({
email: 'user@example.com',
name: 'John Doe',
profile: { create: { bio: 'Software developer' } },
});
async function createUser(data: typeof createUserInput) {
return prisma.user.create({ data });
}
// Where clause validator
const activeUsersWhere = Prisma.validator<Prisma.UserWhereInput>()({
isActive: true,
deletedAt: null,
posts: { some: { published: true } },
});
async;
function findActiveUsers() {
return prisma.user.findMany({ where: activeUsersWhere });
}
```
## GetPayload Patterns
Infer types from query shapes using `GetPayload`.
```typescript
// Complex selections
const postWithAuthor = Prisma.validator<Prisma.PostDefaultArgs>()({
select: {
id: true,
title: true,
content: true,
author: { select: { id: true, name: true, email: true } },
tags: { select: { id: true, name: true } },
},
});
type PostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>;
async function getPostDetails(id: string): Promise<PostWithAuthor | null> {
return prisma.post.findUnique({
where: { id },
...postWithAuthor,
});
}
// Nested GetPayload
type UserWithPostsAndComments = Prisma.UserGetPayload<{
include: {
posts: {
include: {
comments: { include: { author: true } };
};
};
};
}>;
const userArgs = Prisma.validator<Prisma.UserDefaultArgs>()({
include: {
posts: {
include: {
comments: { include: { author: true } },
},
},
},
});
async function getUserWithActivity(id: string): Promise<UserWithPostsAndComments | null> {
return prisma.user.findUnique({
where: { id },
...userArgs,
});
}
```
## Avoiding `any`
Leverage TypeScript's type system instead of `any`.
```typescript
// Type-safe partial selections
function buildUserSelect<T extends Prisma.UserSelect>(
select: T
): Prisma.UserGetPayload<{ select: T }> | null {
return prisma.user.findFirst({ select }) as any;
}
type UserBasic = Prisma.UserGetPayload<{
select: { id: true; email: true; name: true };
}>;
const userSelect = Prisma.validator<Prisma.UserSelect>()({
id: true,
email: true,
name: true,
});
async function getUserBasic(id: string): Promise<UserBasic | null> {
return prisma.user.findUnique({
where: { id },
select: userSelect,
});
}
// Type-safe query builders
class TypedQueryBuilder<T> {
constructor(private model: string) {}
async findMany<A extends Prisma.UserDefaultArgs>(args?: A): Promise<Prisma.UserGetPayload<A>[]> {
return prisma.user.findMany(args);
}
async findUnique<A extends Prisma.UserDefaultArgs>(
args: Prisma.SelectSubset<A, Prisma.UserFindUniqueArgs>
): Promise<Prisma.UserGetPayload<A> | null> {
return prisma.user.findUnique(args);
}
}
const userQuery = new TypedQueryBuilder('user');
const users = await userQuery.findMany({
where: { isActive: true },
include: { posts: true },
});
// Conditional types
type UserArgs<T extends boolean> = T extends true
? Prisma.UserGetPayload<{ include: { posts: true } }>
: Prisma.UserGetPayload<{ select: { id: true; email: true } }>;
async function getUser<T extends boolean>(
id: string,
includePosts: T
): Promise<UserArgs<T> | null> {
if (includePosts) {
return prisma.user.findUnique({
where: { id },
include: { posts: true },
}) as Promise<UserArgs<T> | null>;
}
return prisma.user.findUnique({
where: { id },
select: { id: true, email: true },
}) as Promise<UserArgs<T> | null>;
}
```
## Advanced Patterns
**Reusable Type-Safe Includes:**
```typescript
const includes = {
userWithProfile: Prisma.validator<Prisma.UserDefaultArgs>()({
include: { profile: true },
}),
userWithPosts: Prisma.validator<Prisma.UserDefaultArgs>()({
include: { posts: { where: { published: true } } },
}),
userComplete: Prisma.validator<Prisma.UserDefaultArgs>()({
include: { profile: true, posts: true, comments: true },
}),
} as const;
type UserWithProfile = Prisma.UserGetPayload<typeof includes.userWithProfile>;
type UserWithPosts = Prisma.UserGetPayload<typeof includes.userWithPosts>;
type UserComplete = Prisma.UserGetPayload<typeof includes.userComplete>;
async function getUserVariant(
id: string,
variant: keyof typeof includes
): Promise<UserWithProfile | UserWithPosts | UserComplete | null> {
return prisma.user.findUnique({
where: { id },
...includes[variant],
});
}
```
**Type-Safe Dynamic Queries:**
```typescript
type DynamicUserArgs = {
includeProfile?: boolean;
includePosts?: boolean;
includeComments?: boolean;
};
function buildUserArgs(options: DynamicUserArgs): Prisma.UserDefaultArgs {
const args: Prisma.UserDefaultArgs = {};
if (options.includeProfile || options.includePosts || options.includeComments) {
args.include = {};
if (options.includeProfile) args.include.profile = true;
if (options.includePosts) args.include.posts = true;
if (options.includeComments) args.include.comments = true;
}
return args;
}
async function getDynamicUser(id: string, options: DynamicUserArgs) {
return prisma.user.findUnique({
where: { id },
...buildUserArgs(options),
});
}
```
## Key Principles
1. Always use generated types from `@prisma/client`
2. Use `Prisma.validator` for reusable query fragments
3. Derive types from query shapes via `GetPayload`
4. Avoid `any`; leverage TypeScript's type system
5. Type dynamic queries with proper generics
6. Create const validators for common patterns
## Related Skills
**TypeScript Type Safety:**
- If avoiding any types in TypeScript, use the avoiding-any-types skill from typescript for strict type patterns
- If implementing generic patterns for type-safe queries, use the using-generics skill from typescript for advanced generic techniques

View File

@@ -0,0 +1,509 @@
---
name: handling-transaction-errors
description: Handle transaction errors properly with P-code checking and timeout configuration. Use when implementing transaction error recovery.
allowed-tools: Read, Write, Edit
---
# Transaction Error Handling
Handle transaction errors properly with P-code checking, timeout configuration, and recovery patterns.
## Error Catching in Transactions
All transaction operations must be wrapped in try/catch blocks to handle failures gracefully.
```typescript
try {
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'user@example.com' }
});
await tx.profile.create({
data: { userId: user.id, bio: 'Hello' }
});
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(`Transaction failed: ${error.code}`);
}
throw error;
}
```
## P-Code Error Handling
### P2002: Unique Constraint Violation
```typescript
import { Prisma } from '@prisma/client';
try {
await prisma.$transaction(async (tx) => {
await tx.user.create({
data: { email: 'duplicate@example.com' }
});
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
const target = error.meta?.target as string[];
throw new Error(`Unique constraint failed on: ${target.join(', ')}`);
}
throw error;
}
```
### P2025: Record Not Found
```typescript
try {
await prisma.$transaction(async (tx) => {
const user = await tx.user.update({
where: { id: nonExistentId },
data: { name: 'New Name' }
});
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
) {
throw new Error('Record to update not found');
}
throw error;
}
```
### Comprehensive P-Code Handler
```typescript
function handlePrismaError(error: unknown): Error {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002':
return new Error(
`Unique constraint violation: ${error.meta?.target}`
);
case 'P2025':
return new Error('Record not found');
case 'P2034':
return new Error('Transaction conflict, please retry');
default:
return new Error(`Database error: ${error.code}`);
}
}
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
return new Error('Unknown database error');
}
if (error instanceof Prisma.PrismaClientValidationError) {
return new Error('Invalid query parameters');
}
return error instanceof Error ? error : new Error('Unknown error');
}
try {
await prisma.$transaction(async (tx) => {
await tx.user.create({ data: { email: 'test@example.com' } });
});
} catch (error) {
throw handlePrismaError(error);
}
```
## Timeout Configuration
### Basic Timeout Settings
```typescript
await prisma.$transaction(
async (tx) => {
await tx.user.create({ data: { email: 'user@example.com' } });
await tx.profile.create({ data: { userId: 1, bio: 'Bio' } });
},
{
maxWait: 5000,
timeout: 10000,
}
);
```
Configuration:
- `maxWait`: Maximum time (ms) to wait for transaction to start (default: 2000)
- `timeout`: Maximum time (ms) for transaction to complete (default: 5000)
### Handling Timeout Errors
```typescript
try {
await prisma.$transaction(
async (tx) => {
await tx.user.findMany();
await new Promise(resolve => setTimeout(resolve, 15000));
},
{ timeout: 10000 }
);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.message.includes('timeout')) {
throw new Error('Transaction timed out, please try again');
}
}
throw error;
}
```
### Long-Running Transactions
```typescript
await prisma.$transaction(
async (tx) => {
const users = await tx.user.findMany();
for (const user of users) {
await tx.auditLog.create({
data: {
userId: user.id,
action: 'BATCH_UPDATE',
timestamp: new Date(),
}
});
}
},
{
maxWait: 10000,
timeout: 60000,
}
);
```
## Recovery Patterns
### Retry Strategy with Exponential Backoff
```typescript
async function transactionWithRetry<T>(
operation: (tx: Prisma.TransactionClient) => Promise<T>,
maxRetries = 3
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await prisma.$transaction(operation, {
timeout: 10000,
});
} catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2034'
) {
const delay = Math.pow(2, attempt) * 100;
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error(`Transaction failed after ${maxRetries} retries: ${lastError?.message}`);
}
const result = await transactionWithRetry(async (tx) => {
return await tx.user.create({
data: { email: 'user@example.com' }
});
});
```
### Idempotent Retry Pattern
```typescript
async function upsertWithRetry(email: string, name: string) {
try {
return await prisma.$transaction(async (tx) => {
return await tx.user.upsert({
where: { email },
create: { email, name },
update: { name },
});
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
return await prisma.user.update({
where: { email },
data: { name },
});
}
throw error;
}
}
```
### Graceful Degradation
```typescript
async function transferFunds(fromId: number, toId: number, amount: number) {
try {
return await prisma.$transaction(
async (tx) => {
const from = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } },
});
if (from.balance < 0) {
throw new Error('Insufficient funds');
}
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } },
});
return { success: true };
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
timeout: 5000,
}
);
} catch (error) {
if (error instanceof Error && error.message === 'Insufficient funds') {
return { success: false, reason: 'insufficient_funds' };
}
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
) {
return { success: false, reason: 'account_not_found' };
}
throw error;
}
}
```
### Compensating Transactions
```typescript
async function createOrderWithInventory(
productId: number,
quantity: number,
userId: number
) {
let orderId: number | null = null;
try {
const result = await prisma.$transaction(async (tx) => {
const product = await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } },
});
if (product.stock < 0) {
throw new Error('Insufficient stock');
}
const order = await tx.order.create({
data: {
userId,
productId,
quantity,
status: 'PENDING',
},
});
orderId = order.id;
return order;
});
return result;
} catch (error) {
if (orderId) {
await prisma.order.update({
where: { id: orderId },
data: { status: 'FAILED' },
});
}
throw error;
}
}
```
## Isolation Level Error Handling
```typescript
try {
await prisma.$transaction(
async (tx) => {
const balance = await tx.account.findUnique({
where: { id: accountId },
});
await tx.account.update({
where: { id: accountId },
data: { balance: balance!.balance + amount },
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
}
);
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2034'
) {
throw new Error('Serialization failure, transaction will be retried');
}
throw error;
}
```
## Common Patterns
### Validation Before Transaction
```typescript
async function createUserWithProfile(email: string, name: string) {
const existing = await prisma.user.findUnique({
where: { email },
});
if (existing) {
throw new Error('User already exists');
}
try {
return await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email, name },
});
await tx.profile.create({
data: { userId: user.id },
});
return user;
});
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
throw new Error('User was created by another request');
}
throw error;
}
}
```
### Nested Error Context
```typescript
class TransactionError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly context?: Record<string, unknown>
) {
super(message);
this.name = 'TransactionError';
}
}
async function complexTransaction(data: unknown) {
try {
return await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: data as Prisma.UserCreateInput,
});
return user;
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new TransactionError(
'Transaction failed',
error.code,
{ meta: error.meta, data }
);
}
throw error;
}
}
```
## Anti-Patterns
### DON'T: Ignore Error Types
```typescript
try {
await prisma.$transaction(async (tx) => {
await tx.user.create({ data: { email: 'test@example.com' } });
});
} catch (error) {
console.error('Error occurred');
}
```
### DO: Handle Specific Error Types
```typescript
try {
await prisma.$transaction(async (tx) => {
await tx.user.create({ data: { email: 'test@example.com' } });
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(`Database error ${error.code}: ${error.message}`);
} else {
console.error('Unexpected error:', error);
}
throw error;
}
```
### DON'T: Use Default Timeouts for Long Operations
```typescript
await prisma.$transaction(async (tx) => {
for (let i = 0; i < 10000; i++) {
await tx.log.create({ data: { message: `Log ${i}` } });
}
});
```
### DO: Configure Appropriate Timeouts
```typescript
await prisma.$transaction(
async (tx) => {
const logs = Array.from({ length: 10000 }, (_, i) => ({
message: `Log ${i}`,
}));
await tx.log.createMany({ data: logs });
},
{ timeout: 30000 }
);
```
## Related Skills
**TypeScript Error Handling:**
- If implementing runtime checks for error codes, use the using-runtime-checks skill from typescript for assertion and guard patterns

View File

@@ -0,0 +1,239 @@
---
name: implementing-query-caching
description: Implement query result caching with Redis and proper invalidation strategies for Prisma 6. Use when optimizing frequently accessed data, improving read-heavy application performance, or reducing database load through caching.
allowed-tools: Read, Write, Edit
version: 1.0.0
---
# Query Result Caching with Redis
Efficient query result caching for Prisma 6 applications using Redis: cache key generation, invalidation strategies, TTL management, and when caching provides value.
---
<role>
Implement query result caching with Redis for Prisma 6, covering cache key generation, invalidation, TTL strategies, and identifying when caching delivers value.
</role>
<when-to-activate>
User mentions: caching, Redis, performance optimization, slow queries, read-heavy applications, frequently accessed data, reducing database load, improving response times, cache invalidation, cache warming, or optimizing Prisma queries.
</when-to-activate>
<overview>
Query caching reduces database load and improves read response times, but adds complexity: cache invalidation, consistency challenges, infrastructure. Key capabilities: Redis-Prisma integration, consistent cache key patterns, mutation-triggered invalidation, TTL strategies (time/event-based), and identifying when caching provides value.
</overview>
<workflow>
**Phase 1: Identify Cache Candidates**
Analyze query patterns for read-heavy operations; identify data with acceptable staleness; measure baseline query performance; estimate cache hit rate and improvement.
**Phase 2: Implement Cache Layer**
Set up Redis with connection pooling; create cache wrapper around Prisma queries; implement consistent cache key generation; add cache read with database fallback.
**Phase 3: Implement Invalidation**
Identify mutations affecting cached data; add invalidation to update/delete operations; handle bulk operations and cascading invalidation; test across scenarios.
**Phase 4: Configure TTL**
Determine appropriate TTL per data type; implement time-based expiration; add event-based invalidation for critical data; monitor hit rates and adjust.
</workflow>
<decision-tree>
## When to Cache
**Strong Candidates:**
- Read-heavy data (>10:1 ratio): user profiles, product catalogs, configuration, content lists
- Expensive queries: large aggregations, multi-join, complex filtering, computed values
- High-frequency access
: homepage data, navigation, popular results, trending content
**Weak Candidates:**
- Write-heavy data (<3:1 ratio): analytics, activity logs, messages, live updates
- Frequently changing: stock prices, inventory, bids, live scores
- User-specific: shopping carts, drafts, recommendations, sessions
- Fast simple queries: primary key lookups, indexed queries, already in DB cache
**Decision Tree:**
```
Read/write ratio > 10:1?
├─ Yes: Strong candidate
│ └─ Data stale 1+ minutes acceptable?
│ ├─ Yes: Long TTL (5-60min) + event invalidation
│ └─ No: Short TTL (10-60sec) + aggressive invalidation
└─ No: Ratio > 3:1?
├─ Yes: Moderate candidate, if query > 100ms → short TTL (30-120sec)
└─ No: Skip; optimize query/indexes/pooling instead
```
</decision-tree>
<examples>
## Basic Cache Implementation
**Example 1: Cache-Aside Pattern**
```typescript
import { PrismaClient } from '@prisma/client';
import { Redis } from 'ioredis';
const prisma = new PrismaClient();
const redis = new Redis({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
maxRetriesPerRequest: 3,
});
async function getCachedUser(userId: string) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, email: true, name: true, role: true },
});
if (user) await redis.setex(cacheKey, 300, JSON.stringify(user));
return user;
}
```
**Example 2: Consistent Key Generation**
```typescript
import crypto from 'crypto';
function generateCacheKey(entity: string, query: Record<string, unknown>): string {
const sortedQuery = Object.keys(query)
.sort()
.reduce((acc, key) => {
acc[key] = query[key];
return acc;
}, {} as Record<string, unknown>);
const queryHash = crypto
.createHash('sha256')
.update(JSON.stringify(sortedQuery))
.digest('hex')
.slice(0, 16);
return `${entity}:${queryHash}`;
}
async function getCachedPosts(filters: {
authorId?: string;
published?: boolean;
tags?: string[];
}) {
const cacheKey = generateCacheKey('posts', filters);
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const posts = await prisma.post.findMany({
where: filters,
select: { id: true, title: true, createdAt: true },
});
await redis.setex(cacheKey, 120, JSON.stringify(posts));
return posts;
}
```
**Example 3: Cache Invalidation on Mutation**
```typescript
async function updatePost(postId: string, data: { title?: string; content?: string }) {
const post = await prisma.post.update({ where: { id: postId }, data });
await Promise.all([
redis.del(`post:${postId}`),
redis.del(`posts:author:${post.authorId}`),
redis.keys('posts:*').then((keys) => keys.length > 0 && redis.del(...keys)),
]);
return post;
}
```
**Note:** redis.keys() with patterns is slow on large keysets; use SCAN or maintain key sets.
**Example 4: TTL Strategy**
```typescript
const TTL = {
user_profile: 600,
user_settings: 300,
posts_list: 120,
post_detail: 180,
popular_posts: 60,
real_time_stats: 10,
};
async function cacheWithTTL<T>(
key: string,
ttlType: keyof typeof TTL,
fetchFn: () => Promise<T>
): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const data = await fetchFn();
await redis.setex(key, TTL[ttlType], JSON.stringify(data));
return data;
}
```
</examples>
<constraints>
**MUST:**
* Use cache-aside pattern (not cache-through)
* Consistent cache key generation (no random/timestamp components)
* Invalidate cache on all mutations affecting cached data
* Graceful Redis failure handling with database fallback
* JSON serialization (consistent with Prisma types)
* TTL on all cached values (never infinite)
* Thorough cache invalidation testing
**SHOULD:**
- Redis connection pooling (ioredis)
- Separate cache logic from business logic
- Monitor cache hit rates; adjust TTL accordingly
- Shorter TTL for frequently changing data
- Cache warming for predictably popular data
- Document cache key patterns and invalidation rules
- Use
Redis SCAN vs KEYS for pattern matching
**NEVER:**
- Cache authentication tokens or sensitive credentials
- Use infinite TTL
- Pattern-match invalidation in hot paths
- Cache Prisma queries with skip/take without pagination in key
- Assume cache always available
- Store Prisma instances directly (serialize first)
- Cache write-heavy data
</constraints>
<validation>
**Cache Hit Rate:** Monitor >60% for effective caching; <40% signals strategy reconsideration or TTL adjustment.
**Invalidation Testing:** Verify all mutations invalidate correct keys; test cascading invalidation for related entities; confirm bulk operations invalidate list caches; ensure no stale data post-mutation.
**Performance:** Measure query latency with/without cache; target >50% latency reduction; monitor P95/P99 improvements; verify caching doesn't increase memory pressure.
**Redis Health:** Monitor connection pool utilization, memory usage (set maxmemory-policy), connection failures; test application behavior when Redis is unavailable.
</validation>
---
## References
- [Redis Configuration](./references/redis-configuration.md) — Connection setup, serverless
- [Invalidation Patterns](./references/invalidation-patterns.md) — Event-based, time-based, hybrid
- [Advanced Examples](./references/advanced-examples.md) — Bulk invalidation, cache warming
- [Common Pitfalls](./references/common-pitfalls.md) — Infinite TTL, key inconsistency, missing invalidation

View File

@@ -0,0 +1,183 @@
# Advanced Caching Examples
## Bulk Invalidation
**Invalidate multiple related keys efficiently:**
```typescript
async function invalidateUserCache(userId: string) {
const patterns = [
`user:${userId}`,
`user_profile:${userId}`,
`user_settings:${userId}`,
`posts:author:${userId}`,
`comments:author:${userId}`,
]
await redis.del(...patterns)
}
async function invalidatePostCache(postId: string) {
const post = await prisma.post.findUnique({
where: { id: postId },
select: { authorId: true },
})
if (!post) return
const keys = await redis.keys(`posts:*`)
await Promise.all([
redis.del(`post:${postId}`),
redis.del(`posts:author:${post.authorId}`),
keys.length > 0 ? redis.del(...keys) : Promise.resolve(),
])
}
```
**Pattern:** Collect all related keys and invalidate in a single operation to maintain consistency.
## Cache Warming
**Pre-populate cache with frequently accessed data:**
```typescript
async function warmCache() {
const popularPosts = await prisma.post.findMany({
where: { published: true },
orderBy: { views: 'desc' },
take: 20,
})
await Promise.all(
popularPosts.map(post =>
redis.setex(
`post:${post.id}`,
300,
JSON.stringify(post)
)
)
)
const activeUsers = await prisma.user.findMany({
where: { lastActiveAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) } },
take: 50,
})
await Promise.all(
activeUsers.map(user =>
redis.setex(
`user:${user.id}`,
600,
JSON.stringify(user)
)
)
)
}
```
**Pattern:** Pre-populate cache on application startup or scheduled intervals for predictably popular data.
## Graceful Fallback
**Handle Redis failures without breaking application:**
```typescript
async function getCachedData<T>(
key: string,
fetchFn: () => Promise<T>
): Promise<T> {
try {
const cached = await redis.get(key)
if (cached) {
return JSON.parse(cached)
}
} catch (err) {
console.error('Redis error, falling back to database:', err)
}
const data = await fetchFn()
try {
await redis.setex(key, 300, JSON.stringify(data))
} catch (err) {
console.error('Failed to cache data:', err)
}
return data
}
async function getUserProfile(userId: string) {
return getCachedData(
`user_profile:${userId}`,
() => prisma.user.findUnique({
where: { id: userId },
include: { profile: true },
})
)
}
```
**Pattern:** Wrap all Redis operations in try/catch, always fallback to database on error.
## Advanced TTL Strategy
**Multi-tier caching with different TTL per tier:**
```typescript
const CACHE_TIERS = {
hot: 60,
warm: 300,
cold: 1800,
}
interface CacheOptions {
tier: keyof typeof CACHE_TIERS
keyPrefix: string
}
async function tieredCache<T>(
identifier: string,
options: CacheOptions,
fetchFn: () => Promise<T>
): Promise<T> {
const cacheKey = `${options.keyPrefix}:${identifier}`
const ttl = CACHE_TIERS[options.tier]
const cached = await redis.get(cacheKey)
if (cached) {
return JSON.parse(cached)
}
const data = await fetchFn()
await redis.setex(cacheKey, ttl, JSON.stringify(data))
return data
}
async function getTrendingPosts() {
return tieredCache(
'trending',
{ tier: 'hot', keyPrefix: 'posts' },
() => prisma.post.findMany({
where: { published: true },
orderBy: { views: 'desc' },
take: 10,
})
)
}
async function getArchivedPosts() {
return tieredCache(
'archived',
{ tier: 'cold', keyPrefix: 'posts' },
() => prisma.post.findMany({
where: { archived: true },
orderBy: { archivedAt: 'desc' },
take: 20,
})
)
}
```
**Pattern:** Classify data into tiers based on access patterns, assign appropriate TTL per tier.

View File

@@ -0,0 +1,160 @@
# Common Pitfalls
## Pitfall 1: Infinite TTL
**Problem:** Setting cache values without TTL leads to stale data and memory growth.
**Solution:** Always use `setex()` or `set()` with `EX` option. Never use `set()` alone.
```typescript
await redis.setex(key, 300, value)
```
## Pitfall 2: Cache Key Inconsistency
**Problem:** Query parameter order affects cache key, causing cache misses.
**Solution:** Sort object keys before hashing or use deterministic key generation.
```typescript
function generateKey(obj: Record<string, unknown>) {
const sorted = Object.keys(obj).sort().reduce((acc, key) => {
acc[key] = obj[key]
return acc
}, {} as Record<string, unknown>)
return JSON.stringify(sorted)
}
```
## Pitfall 3: Missing Invalidation Paths
**Problem:** Cache invalidated on direct updates but not on related mutations.
**Solution:** Map all mutation paths and ensure comprehensive invalidation.
```typescript
async function deleteUser(userId: string) {
await prisma.user.delete({ where: { id: userId } })
await Promise.all([
redis.del(`user:${userId}`),
redis.del(`posts:author:${userId}`),
redis.del(`comments:author:${userId}`),
])
}
```
## Pitfall 4: Caching Pagination Without Page Number
**Problem:** Different pages cached with same key, returning wrong results.
**Solution:** Include skip/take or cursor in cache key.
```typescript
const cacheKey = `posts:skip:${skip}:take:${take}`
```
## Pitfall 5: No Redis Fallback
**Problem:** Application crashes when Redis unavailable.
**Solution:** Wrap Redis operations in try/catch, fallback to database.
```typescript
async function getCachedData(key: string, fetchFn: () => Promise<unknown>) {
try {
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
} catch (err) {
console.error('Redis error, falling back to database:', err)
}
return fetchFn()
}
```
## Pitfall 6: Caching Sensitive Data
**Problem:** Storing passwords, tokens, or sensitive credentials in cache.
**Solution:** Never cache authentication tokens, passwords, or PII without encryption.
```typescript
async function getCachedUser(userId: string) {
const cacheKey = `user:${userId}`
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
name: true,
role: true,
},
})
if (user) {
await redis.setex(cacheKey, 300, JSON.stringify(user))
}
return user
}
```
## Pitfall 7: Pattern Matching in Hot Paths
**Problem:** Using `redis.keys('pattern:*')` in high-traffic endpoints causes performance degradation.
**Solution:** Use Redis SCAN for pattern matching or maintain explicit key sets.
```typescript
async function invalidatePostCacheSafe(postId: string) {
const cursor = '0'
const pattern = 'posts:*'
const keysToDelete: string[] = []
let currentCursor = cursor
do {
const [nextCursor, keys] = await redis.scan(
currentCursor,
'MATCH',
pattern,
'COUNT',
100
)
keysToDelete.push(...keys)
currentCursor = nextCursor
} while (currentCursor !== '0')
if (keysToDelete.length > 0) {
await redis.del(...keysToDelete)
}
await redis.del(`post:${postId}`)
}
```
## Pitfall 8: Serialization Issues
**Problem:** Storing Prisma model instances directly without serialization.
**Solution:** Always use JSON.stringify for caching, JSON.parse for retrieval.
```typescript
const user = await prisma.user.findUnique({ where: { id: userId } })
await redis.setex(
`user:${userId}`,
300,
JSON.stringify(user)
)
const cached = await redis.get(`user:${userId}`)
if (cached) {
const user = JSON.parse(cached)
return user
}
```

View File

@@ -0,0 +1,74 @@
# Cache Invalidation Patterns
## Event-Based: Invalidate on Data Changes
Use when: consistency critical, staleness unacceptable.
```typescript
async function createPost(data: { title: string; content: string; authorId: string }) {
const post = await prisma.post.create({ data });
await Promise.all([
redis.del(`posts:author:${data.authorId}`),
redis.del('posts:recent'),
redis.del('posts:popular'),
]);
return post;
}
```
## Time-Based: TTL-Driven Expiration
Use when: staleness acceptable for TTL duration, mutations infrequent.
```typescript
async function getRecentPosts() {
const cached = await redis.get('posts:recent');
if (cached) return JSON.parse(cached);
const posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
await redis.setex('posts:recent', 300, JSON.stringify(posts));
return posts;
}
```
## Hybrid: TTL + Event-Based Invalidation
Use when: mutations trigger immediate invalidation, TTL provides safety net.
```typescript
async function updatePost(postId: string, data: { title?: string }) {
const post = await prisma.post.update({
where: { id: postId },
data,
});
await redis.del(`post:${postId}`);
return post;
}
async function getPost(postId: string) {
const cached = await redis.get(`post:${postId}`);
if (cached) return JSON.parse(cached);
const post = await prisma.post.findUnique({
where: { id: postId },
});
if (post) await redis.setex(`post:${postId}`, 600, JSON.stringify(post));
return post;
}
```
## Strategy Selection by Data Characteristics
| Characteristic | Approach |
| ------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Changes >1/min | Avoid caching or use 5-30s TTL; consider real-time updates; event-based invalidation for consistency |
| Changes rare (hours/days) | Use 5-60min TTL; event-based invalidation on mutations; warm cache on startup |
| Read/write ratio >10:1 | Strong cache candidate; cache-aside pattern; warm popular data in background |
| Read/write ratio <3:1 | Weak candidate; optimize queries instead; cache only if DB bottlenecked |
| Consistency required | Short TTL + event-based invalidation; cache-through/write-behind patterns; add versioning for atomic updates |

View File

@@ -0,0 +1,95 @@
# Redis Configuration
## Connection Setup
**ioredis client with connection pooling:**
```typescript
import { Redis } from 'ioredis'
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: parseInt(process.env.REDIS_DB || '0'),
maxRetriesPerRequest: 3,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000)
return delay
},
lazyConnect: true,
})
redis.on('error', (err) => {
console.error('Redis connection error:', err)
})
redis.on('connect', () => {
console.log('Redis connected')
})
export default redis
```
## Serverless Considerations
**Redis in serverless environments (Vercel, Lambda):**
- Use Redis connection pooling (ioredis handles this)
- Consider Upstash Redis (serverless-optimized)
- Set `lazyConnect: true` to avoid connection on module load
- Handle cold starts gracefully (fallback to database)
- Monitor connection count to avoid exhaustion
**Upstash example:**
```typescript
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
})
```
Upstash uses HTTP REST API, avoiding connection pooling issues in serverless.
## Cache Implementation Checklist
When implementing caching:
**Setup:**
- [ ] Redis client configured with connection pooling
- [ ] Error handling for Redis connection failures
- [ ] Fallback to database when Redis unavailable
- [ ] Environment variables for Redis configuration
**Cache Keys:**
- [ ] Consistent key naming convention (entity:identifier)
- [ ] Hash complex query parameters for deterministic keys
- [ ] Namespace keys by entity type
- [ ] Document key patterns
**Caching Logic:**
- [ ] Cache-aside pattern (read from cache, fallback to DB)
- [ ] Serialize/deserialize with JSON.parse/stringify
- [ ] Handle null/undefined results appropriately
- [ ] Log cache hits/misses for monitoring
**Invalidation:**
- [ ] Invalidate on create/update/delete mutations
- [ ] Handle cascading invalidation for related entities
- [ ] Consider bulk invalidation for list queries
- [ ] Test invalidation across all mutation paths
**TTL Configuration:**
- [ ] Define TTL for each data type
- [ ] Shorter TTL for frequently changing data
- [ ] Longer TTL for static/rarely changing data
- [ ] Document TTL choices and rationale
**Monitoring:**
- [ ] Track cache hit rate
- [ ] Monitor cache memory usage
- [ ] Log invalidation events
- [ ] Alert on Redis connection failures

View File

@@ -0,0 +1,213 @@
---
name: implementing-query-pagination
description: Implement cursor-based or offset pagination for Prisma queries. Use for datasets 100k+, APIs with page navigation, or infinite scroll/pagination mentions.
allowed-tools: Read, Write, Edit, Grep, Glob, Bash
version: 1.0.0
---
# QUERIES-pagination: Efficient Pagination Strategies
Teaches correct Prisma 6 pagination patterns with guidance on cursor vs offset trade-offs and performance implications.
<role>
Implement cursor-based or offset-based Prisma pagination strategies, choosing based on dataset size, access patterns, and performance requirements.
</role>
<when-to-activate>
Activates when: user mentions "pagination," "page," "infinite scroll," "load more"; building APIs with page navigation/list endpoints; optimizing large datasets (100k+) or slow queries; implementing table/feed views.
</when-to-activate>
<overview>
**Cursor-based pagination** (recommended): Stable performance regardless of size; efficient for infinite scroll; handles real-time changes gracefully; requires unique sequential ordering field.
**Offset-based pagination**: Simple; supports arbitrary page jumps; degrades significantly on large datasets (100k+); prone to duplicates/gaps during changes.
\*\*Core principle: Default to cursor. Use offset only for
small (<10k), static datasets requiring arbitrary page access.\*\*
</overview>
<workflow>
## Pagination Strategy Workflow
**Phase 1: Choose Strategy**
- Assess dataset size: <10k (either), 10k100k (prefer cursor), >100k (require cursor)
- Assess access: sequential (cursor); arbitrary jumps (offset); infinite scroll (cursor); traditional pagination (cursor)
- Assess volatility: frequent inserts/deletes (cursor); static (either)
**Phase 2: Implement**
- **Cursor**: select unique ordering field (id, createdAt+id); implement take+cursor+skip; return next cursor; handle edges
- **Offset**: implement take+skip; calculate total pages if needed; validate bounds; document limitations
**Phase 3: Optimize & Validate**
- Add indexes on ordering field(s); test with realistic dataset size; measure performance; document pagination metadata in response
</workflow>
<decision-matrix>
## Pagination Strategy Decision Matrix
| Criterion | Cursor | Offset | Winner |
| ------------------------ | ----------------- | --------------- | ---------- |
| Dataset > 100k | Stable O(n) | O(skip+n) | **Cursor** |
| Infinite scroll | Natural | Poor | **Cursor** |
| Page controls (1,2,3...) | Workaround needed | Natural | Offset |
| Jump to page N | Not supported | Supported | Offset |
| Real-time data | No duplicates | Duplicates/gaps | **Cursor** |
| Total count needed | Extra query | Same query | Offset |
| Complexity | Medium | Low | Offset |
| Mobile feed | Natural | Poor | **Cursor** |
| Admin table (<10k) | Overkill | Simple | Offset |
| Search results | Good | Acceptable | **Cursor** |
**Guidelines:** (1) Default cursor for user-facing lists; (2) Use offset only for small admin tables, total-count requirements, or arbitrary page jumping in internal tools; (3) Never use offset for feeds, timelines, >100k datasets, infinite scroll, real-time data.
</decision-matrix>
<cursor-pagination>
## Cursor-Based Pagination
Cursor pagination uses a pointer to a specific record as the starting point for the next page.
### Basic Pattern
```typescript
async function getPosts(cursor?: string, pageSize: number = 20) {
const posts = await prisma.post.findMany({
take: pageSize,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { id: 'asc' },
});
return {
data: posts,
nextCursor: posts.length === pageSize ? posts[posts.length - 1].id : null,
};
}
```
### Composite Cursor for Non-Unique Ordering
For non-unique fields (createdAt, score), combine with unique field:
```typescript
async function getPostsByDate(cursor?: { createdAt: Date; id: string }, pageSize: number = 20) {
const posts = await prisma.post.findMany({
take: pageSize,
skip: cursor ? 1 : 0,
cursor: cursor ? { createdAt_id: cursor } : undefined,
orderBy: [{ createdAt: 'desc' }, { id: 'asc' }],
});
const lastPost = posts[posts.length - 1];
return {
data: posts,
nextCursor:
posts.length === pageSize ? { createdAt: lastPost.createdAt, id: lastPost.id } : null,
};
}
```
**Schema requirement:**
```prisma
model Post {
id String @id @default(cuid())
createdAt DateTime @default(now())
@@index([createdAt, id])
}
```
### Performance
- **Time complexity**: O(n) where n=pageSize (independent of total dataset size); first and subsequent pages identical
- **Index requirement**: Critical; without index causes full table scan
- **Memory**: Constant (only pageSize records)
- **Data changes**: No duplicates/missing records across pages; new records appear in correct position
</cursor-pagination>
<offset-pagination>
## Offset-Based Pagination
Offset pagination skips a numeric offset of records.
### Basic Pattern
```typescript
async function getPostsPaged(page: number = 1, pageSize: number = 20) {
const skip = (page - 1) * pageSize;
const [posts, total] = await Promise.all([
prisma.post.findMany({ skip, take: pageSize, orderBy: { createdAt: 'desc' } }),
prisma.post.count(),
]);
return {
data: posts,
pagination: { page, pageSize, totalPages: Math.ceil(total / pageSize), totalRecords: total },
};
}
```
### Performance Degradation
**Complexity**: Page 1 O(pageSize); Page N O(N×pageSize)—linear degradation
**Real-world example** (1M records, pageSize 20):
- Page 1 (skip 0): ~5ms
- Page 1,000 (skip 20k): ~150ms
- Page 10,000 (skip 200k): ~1,500ms
- Page 50,000 (skip 1M): ~7,500ms
Database must scan and discard skipped rows despite indexes.
### When Acceptable
Use only when: (1) dataset <10k OR deep pages rare; (2) arbitrary page access required; (3) total count needed; (4) infrequent data changes. Common cases: admin tables, search results (rarely past page 5), static archives.
</offset-pagination>
<validation>
## Validation
1. **Index verification**: Schema has index on ordering field(s); for cursor use `@@index([field1, field2])`; run `npx prisma format`
2. **Performance testing**:
```typescript
console.time('First page');
await getPosts(undefined, 20);
console.timeEnd('First page');
console.time('Page 100');
await getPosts(cursor100, 20);
console.timeEnd('Page 100');
```
Cursor: both ~similar (550ms); Offset: verify acceptable for your use case
3. **Edge cases**: first page, last page (<pageSize results), empty results, invalid cursor/page, concurrent modifications
4. **API contract**: response includes pagination metadata; nextCursor null when done; hasMore accurate; page numbers
validated (>0); consistent ordering across pages; unique fields in composite cursors
</validation>
<constraints>
**MUST**: Index cursor field(s); validate pageSize (max 100); handle empty results; return pagination metadata; use consistent ordering; include unique fields in composite cursors
**SHOULD**: Default cursor for user-facing lists; limit offset to <100k datasets; document pagination strategy; test realistic sizes; consider caching total count
**NEVER**: Use offset for >100k datasets, infinite scroll, feeds/timelines, real-time data; omit indexes; allow unlimited pageSize; use non-unique sole cursor; modify ordering between requests
</constraints>
---
## References
- [Bidirectional Pagination](./references/bidirectional-pagination.md) — Forward/backward navigation
- [Complete API Examples](./references/api-implementation-examples.md) — Full endpoint implementations with filtering
- [Performance Benchmarks](./references/performance-comparison.md) — Detailed performance data, optimization guidance
- [Common Mistakes](./references/common-mistakes.md) — Anti-patterns and fixes
- [Data Change Handling](./references/data-change-handling.md) — Managing duplicates and gaps

View File

@@ -0,0 +1,164 @@
# Complete API Implementation Examples
## Example 1: API Endpoint with Cursor Pagination
```typescript
import { prisma } from './prisma-client';
type GetPostsParams = {
cursor?: string;
limit?: number;
};
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const cursor = searchParams.get('cursor') || undefined;
const limit = Number(searchParams.get('limit')) || 20;
if (limit > 100) {
return Response.json(
{ error: 'Limit cannot exceed 100' },
{ status: 400 }
);
}
const posts = await prisma.post.findMany({
take: limit,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
include: {
author: {
select: { id: true, name: true, email: true },
},
},
});
const nextCursor = posts.length === limit
? posts[posts.length - 1].id
: null;
return Response.json({
data: posts,
nextCursor,
hasMore: nextCursor !== null,
});
}
```
**Client usage:**
```typescript
async function loadMorePosts() {
const response = await fetch(`/api/posts?cursor=${nextCursor}&limit=20`);
const { data, nextCursor: newCursor, hasMore } = await response.json();
setPosts(prev => [...prev, ...data]);
setNextCursor(newCursor);
setHasMore(hasMore);
}
```
## Example 2: Filtered Cursor Pagination
```typescript
type GetFilteredPostsParams = {
cursor?: string;
authorId?: string;
tag?: string;
limit?: number;
};
async function getFilteredPosts({
cursor,
authorId,
tag,
limit = 20,
}: GetFilteredPostsParams) {
const where = {
...(authorId && { authorId }),
...(tag && { tags: { some: { name: tag } } }),
};
const posts = await prisma.post.findMany({
where,
take: limit,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
return {
data: posts,
nextCursor: posts.length === limit ? posts[posts.length - 1].id : null,
};
}
```
**Index requirement:**
```prisma
model Post {
id String @id @default(cuid())
authorId String
createdAt DateTime @default(now())
@@index([authorId, createdAt, id])
}
```
## Example 3: Small Admin Table with Offset
```typescript
type GetAdminUsersParams = {
page?: number;
pageSize?: number;
search?: string;
};
async function getAdminUsers({
page = 1,
pageSize = 50,
search,
}: GetAdminUsersParams) {
const skip = (page - 1) * pageSize;
const where = search
? {
OR: [
{ email: { contains: search, mode: 'insensitive' as const } },
{ name: { contains: search, mode: 'insensitive' as const } },
],
}
: {};
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: pageSize,
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
name: true,
role: true,
createdAt: true,
},
}),
prisma.user.count({ where }),
]);
return {
data: users,
pagination: {
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
totalRecords: total,
hasNext: page < Math.ceil(total / pageSize),
hasPrev: page > 1,
},
};
}
```

View File

@@ -0,0 +1,35 @@
# Bidirectional Pagination
Support both forward and backward navigation in cursor-based pagination.
## Pattern
```typescript
async function getBidirectionalPosts(
cursor?: string,
direction: 'forward' | 'backward' = 'forward',
pageSize: number = 20
) {
const posts = await prisma.post.findMany({
take: direction === 'forward' ? pageSize : -pageSize,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { id: 'asc' },
});
const data = direction === 'backward' ? posts.reverse() : posts;
return {
data,
nextCursor: data.length === pageSize ? data[data.length - 1].id : null,
prevCursor: data.length > 0 ? data[0].id : null,
};
}
```
## Key Points
- Use negative `take` value for backward pagination
- Reverse results when paginating backward
- Return both `nextCursor` and `prevCursor` for navigation
- Maintain consistent ordering across directions

View File

@@ -0,0 +1,84 @@
# Common Pagination Mistakes
## Mistake 1: Using non-unique cursor
**Problem:**
```typescript
cursor: cursor ? { createdAt: cursor } : undefined,
```
Multiple records can have the same `createdAt` value, causing skipped or duplicate records.
**Fix:** Use composite cursor with unique field:
```typescript
cursor: cursor ? { createdAt_id: cursor } : undefined,
orderBy: [{ createdAt: 'desc' }, { id: 'asc' }],
```
## Mistake 2: Missing skip: 1 with cursor
**Problem:**
```typescript
findMany({
cursor: { id: cursor },
take: 20,
})
```
The cursor record itself is included in results, causing duplicate on next page.
**Fix:** Skip cursor record itself:
```typescript
findMany({
cursor: { id: cursor },
skip: 1,
take: 20,
})
```
## Mistake 3: Offset pagination on large datasets
**Problem:**
```typescript
findMany({
skip: page * 1000,
take: 1000,
})
```
Performance degrades linearly with page number on large datasets.
**Fix:** Use cursor pagination:
```typescript
findMany({
cursor: cursor ? { id: cursor } : undefined,
skip: cursor ? 1 : 0,
take: 1000,
})
```
## Mistake 4: Missing index on cursor field
**Problem:**
Schema without index causes full table scans:
```prisma
model Post {
id String @id
createdAt DateTime @default(now())
}
```
**Fix:** Add appropriate index:
```prisma
model Post {
id String @id
createdAt DateTime @default(now())
@@index([createdAt, id])
}
```

View File

@@ -0,0 +1,75 @@
# Handling Data Changes During Pagination
## The Problem
**Offset Pagination Issue:** Duplicates or missing records when data changes between page loads.
### Example Scenario
1. User loads page 1 (posts 1-20)
2. New post is inserted at position 1
3. User loads page 2 (posts 21-40)
4. **Post 21 appears on both pages** (was post 20, now post 21)
### Why It Happens
Offset pagination uses absolute positions:
- Page 1: Records at positions 0-19
- Page 2: Records at positions 20-39
When a record is inserted:
- Page 1 positions: 0-19 (includes new record at position 0)
- Page 2 positions: 20-39 (old position 20 is now position 21)
- **Position 20 was seen on page 1, appears again on page 2**
## Cursor Pagination Solution
Cursor pagination is immune to this problem:
```typescript
const posts = await prisma.post.findMany({
take: 20,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
```
**Why it works:**
- Uses record identity (cursor), not position
- Always starts from the last seen record
- New records appear in correct position
- No duplicates or gaps
## Mitigation for Offset Pagination
If you must use offset pagination:
### Strategy 1: Accept the Limitation
Document behavior for admin tools where occasional duplicates are acceptable.
### Strategy 2: Timestamp Filtering
Create stable snapshots using timestamp filtering:
```typescript
const snapshotTime = new Date();
async function getPage(page: number) {
return await prisma.post.findMany({
where: {
createdAt: { lte: snapshotTime },
},
skip: page * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
});
}
```
**Limitations:**
- Doesn't show new records during pagination session
- User must refresh to see new data
### Strategy 3: Switch to Cursor
The best solution is to redesign using cursor pagination.

View File

@@ -0,0 +1,43 @@
# Performance Comparison
## Benchmark: 500k Posts
**Cursor Pagination (id index):**
- Page 1: 8ms
- Page 100: 9ms
- Page 1000: 10ms
- Page 10000: 11ms
- **Stable performance**
**Offset Pagination (createdAt index):**
- Page 1: 7ms
- Page 100: 95ms
- Page 1000: 890ms
- Page 10000: 8,900ms
- **Linear degradation**
## Memory Usage
Both approaches:
- Load only pageSize records into memory
- Similar memory footprint for same page size
- Database performs filtering/sorting
## Database Load
**Cursor:**
- Index scan from cursor position
- Reads pageSize + 1 rows (for hasMore check)
**Offset:**
- Index scan from beginning
- Skips offset rows (database work, not returned)
- Reads pageSize rows
## Optimization Guidelines
1. **Always add indexes** on ordering fields
2. **Test with realistic data volumes** before production
3. **Monitor query performance** in production
4. **Cache total counts** for offset pagination when possible
5. **Use cursor by default** unless specific requirements demand offset

View File

@@ -0,0 +1,184 @@
---
name: managing-client-lifecycle
description: Manage PrismaClient lifecycle with graceful shutdown, proper disconnect timing, and logging configuration. Use when setting up application shutdown handlers, configuring logging for development or production, or implementing proper connection cleanup in Node.js servers, serverless functions, or test suites.
allowed-tools: Read, Write, Edit
version: 1.0.0
---
# PrismaClient Lifecycle Management
Teaches proper PrismaClient lifecycle patterns for connection cleanup and logging following Prisma 6 best practices.
**Activates when:** Setting up shutdown handlers (SIGINT, SIGTERM), configuring PrismaClient logging, implementing connection cleanup in servers/serverless/tests, writing test teardown logic, or user mentions "shutdown", "disconnect", "cleanup", "logging", "graceful exit".
**Why it matters:** Proper lifecycle management ensures clean connection closure on shutdown, prevents hanging connections from exhausting database resources, provides development/production visibility through logging, and prevents connection leaks in tests.
---
## Implementation Patterns
### Long-Running Servers (Express, Fastify, Custom HTTP)
```typescript
import express from 'express'
import { prisma } from './lib/prisma'
const app = express()
const server = app.listen(3000)
async function gracefulShutdown(signal: string) {
console.log(`Received ${signal}, closing gracefully...`)
server.close(async () => {
await prisma.$disconnect()
process.exit(0)
})
setTimeout(() => { process.exit(1) }, 10000) // Force exit if hung
}
process.on('SIGINT', () => gracefulShutdown('SIGINT'))
process.on('
SIGTERM', () => gracefulShutdown('SIGTERM'))
```
Close HTTP server first (stops new requests), then $disconnect() database, add 10s timeout to force exit if cleanup hangs. Fastify simplifies this with `fastify.addHook('onClose', () => prisma.$disconnect())`.
### Test Suites (Jest, Vitest, Mocha)
```typescript
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
afterAll(async () => {
await prisma.$disconnect();
});
beforeEach(async () => {
await prisma.user.deleteMany(); // Clean data, NOT connections
});
test('creates user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com', name: 'Test' },
});
expect(user.email).toBe('test@example.com');
});
```
Use single PrismaClient instance across all tests; $disconnect() only in afterAll(); clean database state between tests, not connections.
For Vitest setup configuration (setupFiles, global hooks), see `vitest-4/skills/configuring-vitest-4/SKILL.md`.
### Serverless Functions (AWS Lambda, Vercel, Cloudflare Workers)
**Do NOT disconnect in handlers** — breaks warm starts (connection setup every invocation). Use global singleton pattern with connection pooling managed by CLIENT-serverless-config. Exception: RDS Proxy with specific requirements may benefit from explicit $disconnect().
### Next.js
Development: No explicit disconnect needed; Next.js manages lifecycle. Production: Depends on deployment—follow CLIENT-serverless-config for serverless, server pattern for traditional deployment.
---
## Logging Configuration
| Environment | Config | Output |
| --------------- | ----------------------------------------- | ------------------------------------------------------------------- |
| **Development** | `log: ['query', 'info', 'warn', 'error']` | Every SQL query with parameters, connection events, warnings/errors |
| **Production** | `log: ['warn |
', 'error']`| Only warnings and errors; reduced log volume, better performance |
| **Environment-based** |`log: process.env.NODE_ENV === 'production' ? ['warn', 'error'] : ['query', 'info', 'warn', 'error']` | Conditional verbosity |
**Custom event handling:**
```typescript
const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'error' },
{ emit: 'stdout', level: 'warn' },
],
});
prisma.$on('query', (e) => {
console.log(`Query: ${e.query} (${e.duration}ms)`);
});
prisma.$on('error', (e) => {
console.error('Prisma Error:', e);
});
```
---
## Constraints & Validation
**MUST:**
- Call $disconnect() in server shutdown handlers (SIGINT, SIGTERM); in test afterAll/global teardown; await completion before process.exit()
- Use environment-based logging (verbose dev, minimal prod)
**SHOULD:**
- Add 10s timeout to force exit if shutdown hangs
- Close HTTP server before disconnecting database
- Use framework hooks when available (Fastify onClose, NestJS onModuleDestroy)
- Log shutdown progress
**NEVER:**
- Disconnect in serverless function handlers (breaks warm starts)
- Disconnect between test cases (only in afterAll)
- Forget await on $disconnect()
- Exit process before $disconnect() completes
**Validation:**
- Manual: Start server, Ctrl+C, verify "Database connections closed" log and clean exit
- Tests: Run `npm test` — expect no "jest/vitest did not exit" warnings, no connection errors
- Leak detection: Run tests 10x — no "Too many connections" errors or timing degradation
- Logging dev: NODE_ENV=development, verify query logs appear on DB operations
- Logging prod: NODE_ENV=production, verify only warn/error logs appear, successful queries silent
---
## Common Issues & Solutions
| Issue | Cause | Solution |
| ---------------------------------- | ----------------------------------- | ---------------------------------------------------------- |
| "jest/vitest did not exit" warning | Missing $disconnect() in afterAll() | Add `afterAll(async () => { await prisma.$disconnect() })` |
| "
Too many connections" in tests | New PrismaClient created per test file | Use global singleton pattern (see Vitest setup above) |
| Process hangs on shutdown | Forgot await on $disconnect() | Always `await prisma.$disconnect()` |
| Serverless cold starts very slow | Disconnecting in handler breaks warm starts | Remove $disconnect() from handler; use connection pooling |
| Connection pool exhausted after shutdown | $disconnect() called before server.close() | Reverse order: close server first, then disconnect |
---
## Framework-Specific Notes
**Express.js:** Use `server.close()` before `$disconnect()`; handle SIGINT + SIGTERM; add timeout for forced exit.
**Fastify:** Use `onClose` hook—framework handles signal listeners and ordering automatically.
**NestJS:** Implement `onModuleDestroy` lifecycle hook; use `@nestjs/terminus` for health checks; automatic cleanup via module system.
**Next.js:** Dev mode—no explicit disconnect needed. Production—depends on deployment (serverless: see CLIENT-serverless-config; traditional: use server pattern). Server Actions/API Routes—follow serverless pattern.
**Serverless (Lambda, Vercel, Cloudflare):** Default—do NOT disconnect in handlers. Exception—RDS Proxy with specific config. See CLIENT-serverless-config for connection management.
**Test Frameworks:** Jest—`afterAll()` in files or global teardown. Vitest—global `setupFiles`. Mocha—`after()` in root suite. Playwright—`globalTeardown` for E2E.
---
## Related Skills
- **CLIENT-singleton-pattern:** Ensuring single PrismaClient instance
- **CLIENT-serverless-config:** Serverless-specific connection management
- **PERFORMANCE-connection-pooling:** Optimizing connection pool size
**Next.js Integration:**
- If implementing data access layers with session verification, use the securing-data-access-layer skill from nextjs-16 for authenticated database patterns

View File

@@ -0,0 +1,115 @@
---
name: managing-dev-migrations
description: Use migrate dev for versioned migrations; db push for rapid prototyping. Use when developing schema changes locally.
allowed-tools: Read, Write, Edit, Bash
---
## Decision Tree
**Use `prisma migrate dev` when:** Building production-ready features; working on teams with shared schema changes; needing migration history for rollbacks; version controlling schema changes; deploying to staging/production.
**Use `prisma db push` when:** Rapid prototyping and experimentation; early-stage development with frequent schema changes; personal projects without deployment concerns; testing schema ideas quickly; no migration history needed.
## migrate dev Workflow
```bash
npx prisma migrate dev --name add_user_profile
```
Detects schema changes in `schema.prisma`, generates SQL migration, applies to database, regenerates Prisma Client.
Review generated SQL before applying with `--create-only`:
```bash
npx prisma migrate dev --create-only --name add_indexes
```
Edit if needed, then apply:
```bash
npx prisma migrate dev
```
## db push Workflow
```bash
npx prisma db push
```
Syncs `schema.prisma` directly to database without creating migration files; regenerates Prisma Client with warnings on destructive changes. Use for throwaway prototypes or when recreating migrations later.
## Editing Generated Migrations
**When to edit:** Add custom indexes; include
data migrations; optimize generated SQL; add database-specific features.
**Example:** After `--create-only`:
```sql
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"role" TEXT NOT NULL DEFAULT 'user',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
UPDATE "User" SET "role" = 'admin' WHERE "email" = 'admin@example.com';
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
```
Apply:
```bash
npx prisma migrate dev
```
## Workflow Examples
| Scenario | Commands |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Feature Development | `npx prisma migrate dev --name add_comments`; `npx prisma migrate dev --name add_comment_likes`; `npx prisma migrate dev --name add_comment_moderation` |
| Prototyping | `npx prisma db push` (repeat); once stable: `npx prisma migrate dev --name initial_schema` |
| Review & Customize | `npx prisma migrate dev --create-only --name optimize_queries`; edit `prisma/migrations/[timestamp]_optimize_queries/migration.sql`; `npx prisma migrate dev` |
## Switching Between Workflows
**From `db push` to `migrate dev`:** Prisma detects current state and creates baseline migration:
```bash
npx prisma migrate dev --name initial_schema
```
**Handling conflicts** (unapplied migrations + `db push` usage):
```bash
npx prisma migrate reset
npx prisma migrate dev
```
## Common Patterns
| Pattern | Command |
| ------------------ | -------------------------------------------------------------------------------------------------- |
| Daily Development | `npx prisma migrate dev --name descriptive_name` (one per logical change) |
| Experimentation | `npx prisma db push` (until design stable) |
| Pre-commit Review | `npx prisma migrate dev --create-only --name feature_name` (review SQL, commit schema + migration) |
| Team Collaboration | `git pull`; `npx prisma migrate dev` (apply teammate migrations) |
## Troubleshooting
**Migration Already Applied:** Normal when no schema changes exist. Run `npx prisma migrate dev`.
**Drift Detected:** Shows differences between database and migration history:
```bash
npx prisma migrate diff --from-migrations ./prisma/migrations --to-schema
-datamodel ./prisma/schema.prisma
```
Resolve by resetting or creating new migration.
**Data Loss Warnings:** Both commands warn before destructive changes. Review carefully before proceeding; migrate data or adjust schema to cancel.

View File

@@ -0,0 +1,76 @@
---
name: optimizing-query-performance
description: Optimize queries with indexes, batching, and efficient Prisma operations for production performance.
allowed-tools: Read, Write, Edit, Bash
version: 1.0.0
---
<overview>
Query optimization requires strategic indexing, efficient batching, and monitoring to prevent common production anti-patterns.
Key capabilities: Strategic index placement (@@index, @@unique) · Efficient batch operations (createMany, transactions) · Query analysis & N+1 prevention · Field selection optimization & cursor pagination
</overview>
<workflow>
**Phase 1 — Identify:** Enable query logging; analyze patterns/execution times; identify missing indexes, N+1 problems, or inefficient batching
**Phase 2 — Optimize:** Add indexes for filtered/sorted fields; replace loops with batch operations; select only needed fields; use cursor pagination for large datasets
**Phase 3 — Validate:** Measure execution time before/after; verify index usage with EXPLAIN ANALYZE; monitor connection pool under load
</workflow>
## Quick Reference
**Index Strategy:**
| Scenario | Index Type | Example |
| --------------------- | ----------------------------------- | ------------------------------ |
| Single field filter | `@@index([field])` | `@@index([status])` |
| Multiple field filter | `@@index([field1, field2])` | `@@index([userId, status])` |
| Sort + filter | `@@index([filterField, sortField])` | `@@index([status, createdAt])` |
**Batch Operations:**
| Operation | Slow (Loop) | Fast (Batch) |
| --------- | ---------------------- | -------------- |
| Insert | `for...await create()` | `createMany()` |
| Update | `for...await update()` | `updateMany()` |
| Delete | `for...await delete()` | `deleteMany()` |
**Performance Gains:** Indexes (10-100x) · Batch ops (50-100x for 1000+ records) · Cursor pagination (constant vs O(n))
<constraints>
**MUST:** Add indexes for WHERE/ORDER BY/FK fields with frequent queries; use createMany for 100+ records; cursor pagination for deep pagination; select only needed fields; monitor query duration in production
**SHOULD:** Test indexes with production data; chunk 100k+ batches into smaller sizes; use `@@index([field1, field2])` for multi-field filters; remove unused indexes
**NEVER:** Add indexes without performance measurement; offset pagination beyond page 100 on large tables; fetch all
fields when only needing few; loop with individual creates/updates; ignore slow query warnings
</constraints>
<validation>
**Measure Performance:**
```typescript
const start = Date.now()
const result = await prisma.user.findMany({ ... })
console.log(`Query took ${Date.now() - start}ms`)
```
Expected: 50-90% improvement for indexed queries, 50-100x for batch operations
**Verify Index Usage:** Run EXPLAIN ANALYZE; confirm "Index Scan" vs "Seq Scan"
**Monitor Production:** Track P95/P99 latency; expect reduced slow query frequency
**Check Write Performance:** Writes may increase 10-30% per index if rarely-used; consider removal
</validation>
## References
- **Index Strategy**: `references/index-strategy.md` — indexing patterns and trade-offs
- **Batch Operations**: `references/batch-operations.md` — bulk operations and chunking
- **Query Monitoring**: `references/query-monitoring.md` — logging setup and slow query analysis
- **Field Selection**: `references/field-selection.md` — select vs include patterns and N+1 prevention
- **Optimization Examples**: `references/optimization-examples.md` — real-world improvements
- **Next.js Integration**: Next.js plugin for App Router-specific patterns
- **Serverless**: CLIENT-serverless-config skill for connection pooling

View File

@@ -0,0 +1,93 @@
# Batch Operations Guide
## createMany vs Loop
**SLOW (N database round-trips):**
```typescript
for (const userData of users) {
await prisma.user.create({ data: userData })
}
```
**FAST (1 database round-trip):**
```typescript
await prisma.user.createMany({
data: users,
skipDuplicates: true
})
```
**Performance Gain:** 50-100x faster for 1000+ records.
**Limitations:**
- createMany does NOT return created records
- Does NOT trigger middleware or relation cascades
- skipDuplicates skips on unique constraint violations (no error)
## Batch Updates
**SLOW:**
```typescript
for (const id of orderIds) {
await prisma.order.update({
where: { id },
data: { status: 'shipped' }
})
}
```
**FAST:**
```typescript
await prisma.order.updateMany({
where: { id: { in: orderIds } },
data: { status: 'shipped' }
})
```
**Note:** updateMany returns count, not records.
## Batch with Transactions
When you need returned records or relation handling:
```typescript
await prisma.$transaction(
users.map(userData =>
prisma.user.create({ data: userData })
)
)
```
**Use Case:** Creating related records where you need IDs for subsequent operations.
**Trade-off:** Slower than createMany but supports relations and returns records.
## Batch Size Considerations
For very large datasets (100k+ records), chunk into batches:
```typescript
const BATCH_SIZE = 1000
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE)
await prisma.record.createMany({
data: batch,
skipDuplicates: true
})
console.log(`Processed ${Math.min(i + BATCH_SIZE, records.length)}/${records.length}`)
}
```
**Benefits:**
- Progress visibility
- Memory efficiency
- Failure isolation (one batch fails, others succeed)

View File

@@ -0,0 +1,70 @@
# Field Selection Guide
## Select vs Include
**select:** Choose specific fields (excludes all others)
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
email: true
}
})
```
**include:** Add relations to default fields
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
orders: true
}
})
```
**Cannot use both select and include in same query.**
## Nested Selection
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
orders: {
select: {
id: true,
total: true,
createdAt: true
},
where: { status: 'completed' },
orderBy: { createdAt: 'desc' },
take: 5
}
}
})
```
Only fetches recent completed orders, not all orders.
## Counting Relations Without Loading
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
_count: {
select: {
orders: true,
posts: true
}
}
}
})
```
Returns counts without loading actual relation records.

View File

@@ -0,0 +1,91 @@
# Index Strategy Guide
## When to Add Indexes
Add `@@index` for fields that are:
- Frequently used in where clauses
- Used for sorting (orderBy)
- Foreign keys with frequent joins
- Composite conditions used together
## Single-Field Indexes
```prisma
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
status String
@@index([createdAt])
@@index([status])
}
```
**Use Case:**
```typescript
await prisma.user.findMany({
where: { status: 'active' },
orderBy: { createdAt: 'desc' },
take: 20
})
```
Both status filter and createdAt sort benefit from indexes.
## Composite Indexes
```prisma
model Order {
id Int @id @default(autoincrement())
userId Int
status String
createdAt DateTime @default(now())
totalCents Int
user User @relation(fields: [userId], references: [id])
@@index([userId, status])
@@index([status, createdAt])
}
```
**Composite Index Rules:**
1. Order matters: [userId, status] helps queries filtering by userId, or userId + status
2. Does NOT help queries filtering only by status
3. Most selective field should come first
4. Match your most common query patterns
**Use Case:**
```typescript
await prisma.order.findMany({
where: {
userId: 123,
status: 'pending'
},
orderBy: { createdAt: 'desc' }
})
```
First index [userId, status] optimizes the where clause.
Second index [status, createdAt] would help if querying by status alone with date sorting.
## Index Trade-offs
**Benefits:**
- Faster read queries (10-100x improvement on large tables)
- Required for efficient sorting and filtering
- Essential for foreign key performance
**Costs:**
- Slower writes (insert/update/delete must update indexes)
- Storage overhead (5-20% per index)
- Diminishing returns beyond 5-7 indexes per table
**Rule:** Only index fields actually used in queries. Remove unused indexes.

View File

@@ -0,0 +1,153 @@
# Optimization Examples
## Example 1: Add Composite Index for Common Query
**Scenario:** API endpoint filtering orders by userId and status, sorted by date
**Current Schema:**
```prisma
model Order {
id Int @id @default(autoincrement())
userId Int
status String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
```
**Query:**
```typescript
await prisma.order.findMany({
where: {
userId: req.user.id,
status: 'pending'
},
orderBy: { createdAt: 'desc' }
})
```
**Optimization - Add Composite Index:**
```prisma
model Order {
id Int @id @default(autoincrement())
userId Int
status String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId, status, createdAt])
}
```
Index covers filter AND sort, enabling index-only scan.
## Example 2: Optimize Bulk Insert
**Scenario:** Import 10,000 products from CSV
**SLOW Approach:**
```typescript
for (const row of csvData) {
await prisma.product.create({
data: {
name: row.name,
sku: row.sku,
price: parseFloat(row.price)
}
})
}
```
10,000 database round-trips = 60+ seconds
**FAST Approach:**
```typescript
const products = csvData.map(row => ({
name: row.name,
sku: row.sku,
price: parseFloat(row.price)
}))
await prisma.product.createMany({
data: products,
skipDuplicates: true
})
```
1 database round-trip = <1 second
**Even Better - Chunked Batches:**
```typescript
const BATCH_SIZE = 1000
for (let i = 0; i < products.length; i += BATCH_SIZE) {
const batch = products.slice(i, i + BATCH_SIZE)
await prisma.product.createMany({
data: batch,
skipDuplicates: true
})
}
```
Progress tracking + failure isolation.
## Example 3: Identify and Fix Slow Query
**Enable Logging:**
```typescript
const prisma = new PrismaClient({
log: [{ emit: 'event', level: 'query' }]
})
prisma.$on('query', (e) => {
if (e.duration > 500) {
console.log(`SLOW QUERY (${e.duration}ms): ${e.query}`)
}
})
```
**Detected Slow Query:**
```
SLOW QUERY (3421ms): SELECT * FROM "Post" WHERE "published" = true ORDER BY "views" DESC LIMIT 10
```
**Analyze with EXPLAIN:**
```typescript
await prisma.$queryRaw`
EXPLAIN ANALYZE
SELECT * FROM "Post"
WHERE "published" = true
ORDER BY "views" DESC
LIMIT 10
`
```
**Output shows:** Seq Scan (full table scan)
**Solution - Add Index:**
```prisma
model Post {
id Int @id @default(autoincrement())
published Boolean @default(false)
views Int @default(0)
@@index([published, views])
}
```
**Verify Improvement:**
After migration, same query executes in ~15ms (228x faster).

View File

@@ -0,0 +1,131 @@
# Query Monitoring Guide
## Enable Query Logging
**Development:**
```typescript
const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
{ emit: 'stdout', level: 'error' },
{ emit: 'stdout', level: 'warn' }
]
})
prisma.$on('query', (e) => {
console.log('Query: ' + e.query)
console.log('Duration: ' + e.duration + 'ms')
})
```
**Production (structured logging):**
```typescript
const prisma = new PrismaClient({
log: [{ emit: 'event', level: 'query' }]
})
prisma.$on('query', (e) => {
if (e.duration > 1000) {
logger.warn('Slow query detected', {
query: e.query,
duration: e.duration,
params: e.params
})
}
})
```
## Analyzing Slow Queries
**Identify Patterns:**
1. Queries without WHERE clause on large tables (full table scans)
2. Complex JOINs without indexes on foreign keys
3. ORDER BY on unindexed fields
4. Missing LIMIT on large result sets
**Use Database EXPLAIN:**
```typescript
await prisma.$queryRaw`EXPLAIN ANALYZE
SELECT * FROM "User"
WHERE status = 'active'
ORDER BY "createdAt" DESC
LIMIT 20
`
```
Look for:
- "Seq Scan" (sequential scan) - needs index
- "Index Scan" - good
- High execution time relative to query complexity
## Common Query Anti-Patterns
**N+1 Problem:**
```typescript
const users = await prisma.user.findMany()
for (const user of users) {
const orders = await prisma.order.findMany({
where: { userId: user.id }
})
}
```
**Solution - Use include:**
```typescript
const users = await prisma.user.findMany({
include: {
orders: true
}
})
```
**Over-fetching:**
```typescript
const users = await prisma.user.findMany()
```
Fetches ALL fields for ALL users.
**Solution - Select needed fields:**
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true
}
})
```
**Offset Pagination on Large Datasets:**
```typescript
await prisma.user.findMany({
skip: 50000,
take: 20
})
```
Database must scan and skip 50,000 rows.
**Solution - Cursor pagination:**
```typescript
await prisma.user.findMany({
take: 20,
cursor: { id: lastSeenId },
skip: 1
})
```
Constant time regardless of page depth.

View File

@@ -0,0 +1,271 @@
---
name: optimizing-query-selection
description: Optimize queries by selecting only required fields and avoiding N+1 problems. Use when writing queries with relations or large result sets.
allowed-tools: Read, Write, Edit
version: 1.0.0
---
# Query Select Optimization
Optimize Prisma 6 queries through selective field loading and relation batching to prevent N+1 problems and reduce data transfer.
---
<role>
Optimize Prisma 6 queries by selecting required fields only, properly loading relations to prevent N+1 problems while minimizing data transfer and memory usage.
</role>
<when-to-activate>
- Writing user-facing data queries
- Loading models with relations
- Building API endpoints or GraphQL resolvers
- Optimizing slow queries; reducing database load
- Working with large result sets
</when-to-activate>
<workflow>
## Optimization Workflow
1. **Identify:** Determine required fields, relations to load, relation count needs, full vs. specific fields
2. **Choose:** `include` (prototyping, most fields) vs. `select` (production, API responses, performance-critical)
3. **Implement:** Use `select` for precise control, nest relations with `select`, use `_count` instead of loading all records, limit relation results with `take`
4. **Index:** Fields in `where` clauses, `orderBy` fields, composite indexes for filtered relations
5. **Validate:** Enable query logging for single-query verification, test with realistic data volumes, measure payload size and query duration
</workflow>
<core-principles>
## Core Principles
### 1. Select Only Required Fields
**Problem:** Fetching entire models wastes bandwidth and memory
```typescript
const users = await prisma.user.findMany()
```
**Solution:** Use `select` to fetch only needed fields
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
},
})
```
**Performance Impact:**
- Reduces data transfer by 60-90% for models with many fields
- Faster JSON serialization
- Lower memory usage
- Excludes sensitive fields by default
### 2. Include vs Select
**Include:** Adds relations to full model
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
profile: true,
},
})
```
**Select:** Precise control over all fields
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
email: true,
posts: {
select: {
id: true,
title: true,
published: true,
},
},
profile: {
select: {
bio: true,
avatar: true,
},
},
},
})
```
**When to Use:**
- `include`: Quick prototyping, need most fields
- `select`: Production code, API responses, performance-critical paths
### 3. Preventing N+1 Queries
**N+1 Problem:** Separate query for each relation
```typescript
const posts = await prisma.post.findMany()
for (const post of posts) {
const author = await prisma.user.findUnique({
where: { id: post.authorId },
})
}
```
**Solution:** Use `include` or `select` with relations
```typescript
const posts = await prisma.post.findMany({
include: {
author: true,
},
})
```
**Better:** Select only needed author fields
```typescript
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
content: true,
author: {
select: {
id: true,
name: true,
email: true,
},
},
},
})
```
### 4. Relation Counting
**Problem:** Loading all relations just to count them
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
},
})
const postCount = user.posts.length
```
**Solution:** Use `_count` for efficient aggregation
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
id: true,
name: true,
_count: {
select: {
posts: true,
comments: true,
},
},
},
})
```
**Result:**
```typescript
{
id: 1,
name: "Alice",
_count: {
posts: 42,
comments: 128
}
}
```
</core-principles>
<quick-reference>
## Quick Reference
### Optimized Query Pattern
```typescript
const optimized = await prisma.model.findMany({
where: {},
select: {
field1: true,
field2: true,
relation: {
select: {
field: true,
},
take: 10,
},
_count: {
select: {
relation: true,
},
},
},
orderBy: { field: 'desc' },
take: 20,
skip: 0,
})
```
### Key Takeaways
- Default to `select` for all production queries
- Use `include` only for prototyping
- Always use `_count` for counting relations
- Combine selection with filtering and pagination
- Prevent N+1 by loading relations upfront
- Select minimal fields for list views, more for detail views
</quick-reference>
<constraints>
## Constraints and Guidelines
**MUST:**
- Use `select` for all API responses
- Load relations in same query (prevent N+1)
- Use `_count` for relation counts
- Add indexes for filtered/ordered fields
- Test with realistic data volumes
**SHOULD:**
- Limit relation results with `take`
- Create reusable selection objects
- Enable query logging during development
- Measure performance improvements
- Document selection patterns
**NEVER:**
- Use `include` in production without field selection
- Load relations in loops (N+1)
- Fetch full models when only counts needed
- Over-fetch nested relations
- Skip indexes on commonly queried fields
</constraints>
---
## References
For detailed patterns and examples, see:
- [Nested Selection Patterns](./references/nested-selection.md) - Deep relation hierarchies and complex selections
- [API Optimization Patterns](./references/api-optimization.md) - List vs detail views, pagination with select
- [N+1 Prevention Guide](./references/n-plus-one-prevention.md) - Detailed anti-patterns and solutions
- [Type Safety Guide](./references/type-safety.md) - TypeScript types and reusable selection objects
- [Performance Verification](./references/performance-verification.md) - Testing and validation techniques

View File

@@ -0,0 +1,139 @@
# API Optimization Patterns
## API Endpoint Optimization
```typescript
export async function GET(request: Request) {
const posts = await prisma.post.findMany({
where: { published: true },
select: {
id: true,
title: true,
slug: true,
excerpt: true,
publishedAt: true,
author: {
select: {
name: true,
avatar: true,
},
},
_count: {
select: {
comments: true,
},
},
},
orderBy: {
publishedAt: 'desc',
},
take: 20,
})
return Response.json(posts)
}
```
## List vs Detail Views
### List View: Minimal Fields
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
role: true,
_count: {
select: {
posts: true,
},
},
},
})
```
### Detail View: More Complete Data
```typescript
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
role: true,
bio: true,
avatar: true,
createdAt: true,
posts: {
select: {
id: true,
title: true,
publishedAt: true,
_count: {
select: {
comments: true,
},
},
},
orderBy: {
publishedAt: 'desc',
},
take: 10,
},
_count: {
select: {
posts: true,
comments: true,
followers: true,
},
},
},
})
```
## Pagination with Select
```typescript
async function getPaginatedPosts(page: number, pageSize: number) {
const [posts, total] = await Promise.all([
prisma.post.findMany({
select: {
id: true,
title: true,
excerpt: true,
author: {
select: {
name: true,
},
},
},
skip: page * pageSize,
take: pageSize,
orderBy: {
createdAt: 'desc',
},
}),
prisma.post.count(),
])
return {
posts,
pagination: {
page,
pageSize,
total,
pages: Math.ceil(total / pageSize),
},
}
}
```
## Key Patterns
- **List views:** Minimize fields, use `_count` for relations
- **Detail views:** Include necessary relations with limits
- **API responses:** Always use `select` to control shape
- **Pagination:** Combine `select` with `take`/`skip`

View File

@@ -0,0 +1,112 @@
# N+1 Prevention Guide
## Anti-Patterns
### Over-fetching
**Problem:**
```typescript
const user = await prisma.user.findUnique({
where: { id },
include: {
posts: {
include: {
comments: {
include: {
author: true,
},
},
},
},
},
})
```
**Issue:** Fetches thousands of records, massive data transfer
**Fix:** Use select with limits
```typescript
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
name: true,
posts: {
select: {
id: true,
title: true,
_count: {
select: {
comments: true,
},
},
},
take: 10,
orderBy: {
createdAt: 'desc',
},
},
},
})
```
### Inconsistent Selection
**Problem:**
```typescript
const posts = await prisma.post.findMany({
include: {
author: true,
},
})
```
**Issue:** Full author object when only name needed
**Fix:** Select specific fields
```typescript
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
author: {
select: {
name: true,
},
},
},
})
```
### Selecting Then Filtering
**Problem:**
```typescript
const users = await prisma.user.findMany()
const activeUsers = users.filter(u => u.status === 'active')
```
**Issue:** Fetches all users, filters in application
**Fix:** Filter in database
```typescript
const activeUsers = await prisma.user.findMany({
where: { status: 'active' },
select: {
id: true,
name: true,
email: true,
},
})
```
## Prevention Strategies
1. **Always load relations upfront** - Never query in loops
2. **Use select with relations** - Don't fetch unnecessary fields
3. **Add take limits** - Prevent accidental bulk loads
4. **Use _count** - Don't load relations just to count
5. **Test with realistic data** - N+1 only shows at scale

View File

@@ -0,0 +1,81 @@
# Nested Selection Patterns
## Deep Relation Hierarchies
Select fields deep in relation hierarchies:
```typescript
const posts = await prisma.post.findMany({
select: {
title: true,
author: {
select: {
name: true,
profile: {
select: {
avatar: true,
},
},
},
},
comments: {
select: {
content: true,
author: {
select: {
name: true,
},
},
},
take: 5,
orderBy: {
createdAt: 'desc',
},
},
},
})
```
## Combining Select with Filtering
Optimize both data transfer and query performance:
```typescript
const recentPosts = await prisma.post.findMany({
where: {
published: true,
createdAt: {
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
select: {
id: true,
title: true,
excerpt: true,
createdAt: true,
author: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
comments: true,
likes: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
take: 10,
})
```
## Key Principles
- Nest selections to match data shape requirements
- Use `take` on nested relations to prevent over-fetching
- Combine `orderBy` with nested relations for sorted results
- Use `_count` for relation counts instead of loading all records

View File

@@ -0,0 +1,101 @@
# Performance Verification
## Verification Checklist
After optimization, verify improvements:
1. **Data Size:** Check response payload size
2. **Query Time:** Measure database query duration
3. **Query Count:** Ensure single query instead of N+1
4. **Memory Usage:** Monitor application memory
## Enable Query Logging
```typescript
const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'query' },
],
})
prisma.$on('query', (e) => {
console.log('Query: ' + e.query)
console.log('Duration: ' + e.duration + 'ms')
})
```
## Performance Testing
```typescript
async function testQueryPerformance() {
console.time('Unoptimized')
await prisma.user.findMany({
include: { posts: true }
})
console.timeEnd('Unoptimized')
console.time('Optimized')
await prisma.user.findMany({
select: {
id: true,
name: true,
_count: { select: { posts: true } }
}
})
console.timeEnd('Optimized')
}
```
## Payload Size Comparison
```typescript
async function comparePayloadSize() {
const full = await prisma.post.findMany()
const optimized = await prisma.post.findMany({
select: {
id: true,
title: true,
excerpt: true,
}
})
console.log('Full payload:', JSON.stringify(full).length, 'bytes')
console.log('Optimized payload:', JSON.stringify(optimized).length, 'bytes')
console.log('Reduction:',
Math.round((1 - JSON.stringify(optimized).length / JSON.stringify(full).length) * 100),
'%'
)
}
```
## Index Verification
Check that indexes exist for queried fields:
```sql
-- PostgreSQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'Post';
-- MySQL
SHOW INDEXES FROM Post;
```
## Production Monitoring
Monitor in production:
1. **APM tools:** Track query performance over time
2. **Database metrics:** Monitor slow query log
3. **API response times:** Measure endpoint latency
4. **Memory usage:** Track application memory consumption
## Expected Improvements
After optimization:
- **Query count:** Reduced to 1-2 queries (from N+1)
- **Response size:** 60-90% smaller payload
- **Query time:** Similar or faster
- **Memory usage:** 50-80% lower

View File

@@ -0,0 +1,101 @@
# Type Safety Guide
## Inferred Types
TypeScript infers exact return types based on selection:
```typescript
const user = await prisma.user.findUnique({
where: { id: 1 },
select: {
name: true,
email: true,
posts: {
select: {
title: true,
},
},
},
})
```
Inferred type:
```typescript
{
name: string
email: string
posts: {
title: string
}[]
} | null
```
## Reusable Selection Objects
Create reusable selection objects:
```typescript
const userBasicSelect = {
id: true,
name: true,
email: true,
} as const
const users = await prisma.user.findMany({
select: userBasicSelect,
})
```
## Composition Patterns
Build complex selections from smaller pieces:
```typescript
const authorSelect = {
id: true,
name: true,
email: true,
} as const
const postSelect = {
id: true,
title: true,
author: {
select: authorSelect,
},
} as const
const posts = await prisma.post.findMany({
select: postSelect,
})
```
## Type Extraction
Extract types from selection objects:
```typescript
import { Prisma } from '@prisma/client'
const postWithAuthor = Prisma.validator<Prisma.PostDefaultArgs>()({
select: {
id: true,
title: true,
author: {
select: {
id: true,
name: true,
},
},
},
})
type PostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>
```
## Benefits
- **Type safety:** Compiler catches field typos
- **Refactoring:** Changes propagate through types
- **Reusability:** Share selection patterns
- **Documentation:** Types serve as inline docs

View File

@@ -0,0 +1,483 @@
---
name: preventing-error-exposure
description: Prevent leaking database errors and P-codes to clients. Use when implementing API error handling or user-facing error messages.
allowed-tools: Read, Write, Edit
version: 1.0.0
---
# Security: Error Exposure Prevention
This skill teaches Claude how to handle Prisma errors securely by transforming detailed database errors into user-friendly messages while preserving debugging information in logs.
---
<role>
This skill prevents leaking sensitive database information (P-codes, table names, column details, constraints) to API clients while maintaining comprehensive server-side logging for debugging.
</role>
<when-to-activate>
This skill activates when:
- Implementing API error handlers or middleware
- Working with try/catch blocks around Prisma operations
- Building user-facing error responses
- Setting up logging infrastructure
- User mentions error handling, error messages, or API responses
</when-to-activate>
<overview>
Prisma errors contain detailed database information including:
- P-codes (P2002, P2025, etc.) revealing database operations
- Table and column names exposing schema structure
- Constraint names showing relationships
- Query details revealing business logic
**Security Risk:** Exposing this information helps attackers:
- Map database schema
- Identify validation rules
- Craft targeted attacks
- Discover business logic
**Solution Pattern:** Transform errors for clients, log full details server-side.
Key capabilities:
1. P-code to user message transformation
2. Error sanitization removing sensitive details
3. Server-side logging with full context
4. Production-ready error middleware
</overview>
<workflow>
## Standard Workflow
**Phase 1: Error Detection**
1. Wrap Prisma operations in try/catch
2. Identify error type (Prisma vs generic)
3. Extract P-code if present
**Phase 2: Error Transformation**
1. Map P-code to user-friendly message
2. Remove database-specific details
3. Generate safe generic message for unknown errors
4. Preserve error context for logging
**Phase 3: Response and Logging**
1. Log full error details server-side (P-code, stack, query)
2. Return sanitized message to client
3. Include generic error ID for support correlation
</workflow>
<conditional-workflows>
## Production vs Development
**Development Environment:**
- Log full error details including stack traces
- Optionally include P-codes in API response for debugging
- Show detailed validation errors
- Enable query logging
**Production Environment:**
- NEVER expose P-codes to clients
- Log errors with correlation IDs
- Return generic user messages
- Monitor error rates for P2024 (connection timeout)
- Alert on P2002 spikes (potential brute force)
## Framework-Specific Patterns
**Next.js App Router:**
```typescript
export async function POST(request: Request) {
try {
const data = await request.json()
const result = await prisma.user.create({ data })
return Response.json(result)
} catch (error) {
return handlePrismaError(error)
}
}
```
**Express/Fastify:**
```typescript
app.use((err, req, res, next) => {
if (isPrismaError(err)) {
const { status, message, errorId } = transformPrismaError(err)
logger.error({ err, errorId, userId: req.user?.id })
return res.status(status).json({ error: message, errorId })
}
next(err)
})
```
</conditional-workflows>
<examples>
## Example 1: Error Transformation Function
**Pattern: P-code to User Message**
```typescript
import { Prisma } from '@prisma/client'
function transformPrismaError(error: unknown) {
const errorId = crypto.randomUUID()
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002':
return {
status: 409,
message: 'A record with this information already exists.',
errorId,
logDetails: {
code: error.code,
meta: error.meta,
target: error.meta?.target
}
}
case 'P2025':
return {
status: 404,
message: 'The requested resource was not found.',
errorId,
logDetails: {
code: error.code,
meta: error.meta
}
}
case 'P2003':
return {
status: 400,
message: 'The provided reference is invalid.',
errorId,
logDetails: {
code: error.code,
meta: error.meta,
field: error.meta?.field_name
}
}
case 'P2014':
return {
status: 400,
message: 'The change violates a required relationship.',
errorId,
logDetails: {
code: error.code,
meta: error.meta
}
}
default:
return {
status: 500,
message: 'An error occurred while processing your request.',
errorId,
logDetails: {
code: error.code,
meta: error.meta
}
}
}
}
if (error instanceof Prisma.PrismaClientValidationError) {
return {
status: 400,
message: 'The provided data is invalid.',
errorId,
logDetails: {
type: 'ValidationError',
message: error.message
}
}
}
return {
status: 500,
message: 'An unexpected error occurred.',
errorId,
logDetails: {
type: error?.constructor?.name,
message: error instanceof Error ? error.message : 'Unknown error'
}
}
}
```
## Example 2: Production Error Handler
**Pattern: Middleware with Logging**
```typescript
import { Prisma } from '@prisma/client'
import { logger } from './logger'
export function handlePrismaError(error: unknown, context?: Record<string, unknown>) {
const { status, message, errorId, logDetails } = transformPrismaError(error)
logger.error({
errorId,
...logDetails,
context,
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString()
})
return {
status,
body: {
error: message,
errorId
}
}
}
export async function createUser(data: { email: string; name: string }) {
try {
return await prisma.user.create({ data })
} catch (error) {
const { status, body } = handlePrismaError(error, {
operation: 'createUser',
email: data.email
})
throw new ApiError(status, body)
}
}
```
## Example 3: Environment-Aware Error Handling
**Pattern: Development vs Production**
```typescript
const isDevelopment = process.env.NODE_ENV === 'development'
function formatErrorResponse(error: unknown, errorId: string) {
const { status, message, logDetails } = transformPrismaError(error)
const baseResponse = {
error: message,
errorId
}
if (isDevelopment && error instanceof Prisma.PrismaClientKnownRequestError) {
return {
...baseResponse,
debug: {
code: error.code,
meta: error.meta,
clientVersion: Prisma.prismaVersion.client
}
}
}
return baseResponse
}
```
## Example 4: Specific Field Error Extraction
**Pattern: P2002 Constraint Details**
```typescript
function extractP2002Details(error: Prisma.PrismaClientKnownRequestError) {
if (error.code !== 'P2002') return null
const target = error.meta?.target as string[] | undefined
if (!target || target.length === 0) {
return 'A record with this information already exists.'
}
const fieldMap: Record<string, string> = {
email: 'email address',
username: 'username',
phone: 'phone number',
slug: 'identifier'
}
const fieldName = target[0]
const friendlyName = fieldMap[fieldName] || 'information'
return `A record with this ${friendlyName} already exists.`
}
function transformPrismaError(error: unknown) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
const message = extractP2002Details(error)
return {
status: 409,
message,
errorId: crypto.randomUUID(),
logDetails: { code: 'P2002', target: error.meta?.target }
}
}
}
```
</examples>
<output-format>
## Error Response Format
**Client Response (JSON):**
```json
{
"error": "User-friendly message without database details",
"errorId": "uuid-for-correlation"
}
```
**Server Log (Structured):**
```json
{
"level": "error",
"errorId": "uuid-for-correlation",
"code": "P2002",
"meta": { "target": ["email"] },
"context": { "operation": "createUser", "userId": "123" },
"stack": "Error stack trace...",
"timestamp": "2025-11-21T10:30:00Z"
}
```
**Development Response (Optional Debug):**
```json
{
"error": "User-friendly message",
"errorId": "uuid-for-correlation",
"debug": {
"code": "P2002",
"meta": { "target": ["email"] },
"clientVersion": "6.0.0"
}
}
```
</output-format>
<constraints>
## Security Requirements
**MUST:**
- Transform ALL Prisma errors before sending to clients
- Log full error details server-side with correlation IDs
- Remove P-codes from production API responses
- Remove table/column names from client messages
- Remove constraint names from client messages
- Use generic messages for unexpected errors
**SHOULD:**
- Include error IDs for support correlation
- Monitor error rates for security patterns
- Use structured logging for error analysis
- Implement field-specific messages for P2002
- Differentiate 404 (P2025) from 400/500 errors
**NEVER:**
- Expose P-codes to clients in production
- Include error.meta in API responses
- Show Prisma stack traces to clients
- Reveal table or column names
- Display constraint names
- Return raw error.message to clients
## Common P-codes to Handle
**P2002** - Unique constraint violation
- Status: 409 Conflict
- Message: "A record with this information already exists"
**P2025** - Record not found
- Status: 404 Not Found
- Message: "The requested resource was not found"
**P2003** - Foreign key constraint violation
- Status: 400 Bad Request
- Message: "The provided reference is invalid"
**P2014** - Required relation violation
- Status: 400 Bad Request
- Message: "The change violates a required relationship"
**P2024** - Connection timeout
- Status: 503 Service Unavailable
- Message: "Service temporarily unavailable"
- Action: Log urgently, indicates connection pool exhaustion
</constraints>
<validation>
## Security Checklist
After implementing error handling:
1. **Verify No P-codes Exposed:**
- Search API responses for "P20" pattern
- Test each error scenario
- Check production logs vs API responses
2. **Confirm Logging Works:**
- Trigger known errors (P2002, P2025)
- Verify errorId appears in both logs and response
- Confirm full error details in logs only
3. **Test Error Scenarios:**
- Unique constraint violation (create duplicate)
- Not found (query non-existent record)
- Foreign key violation (invalid reference)
- Validation error (missing required field)
4. **Review Environment Behavior:**
- Production: No P-codes, no meta, no stack
- Development: Optional debug info
- Logs: Full details in both environments
</validation>
---
## Integration with SECURITY-input-validation
Error exposure prevention works with input validation:
1. **Input Validation** (SECURITY-input-validation skill):
- Validate data before Prisma operations
- Return validation errors with field-level messages
- Prevent malformed data reaching database
2. **Error Transformation** (this skill):
- Handle database-level errors
- Transform Prisma errors to user messages
- Log server-side for debugging
**Pattern:**
```typescript
async function createUser(input: unknown) {
const validation = userSchema.safeParse(input)
if (!validation.success) {
return {
status: 400,
body: {
error: 'Invalid user data',
fields: validation.error.flatten().fieldErrors
}
}
}
try {
return await prisma.user.create({ data: validation.data })
} catch (error) {
const { status, body } = handlePrismaError(error)
return { status, body }
}
}
```
Validation catches format issues, error transformation handles database constraints.
## Related Skills
**Error Handling and Validation:**
- If sanitizing error messages for user display, use the sanitizing-user-inputs skill from typescript for safe error formatting
- If customizing Zod validation errors, use the customizing-errors skill from zod-4 for user-friendly error messages

View File

@@ -0,0 +1,441 @@
---
name: preventing-sql-injection
description: Prevent SQL injection by using $queryRaw tagged templates instead of $queryRawUnsafe. Use when writing raw SQL queries or dynamic queries.
allowed-tools: Read, Write, Edit, Grep
---
# SQL Injection Prevention in Prisma 6
## Overview
SQL injection is one of the most critical security vulnerabilities in database applications. In Prisma 6, raw SQL queries must be written using `$queryRaw` tagged templates for automatic parameterization. **NEVER use `$queryRawUnsafe` with user input.**
## Critical Rules
### 1. ALWAYS Use $queryRaw Tagged Templates
```typescript
const email = userInput;
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${email}
`;
```
Prisma automatically parameterizes `${email}` to prevent SQL injection.
### 2. NEVER Use $queryRawUnsafe with User Input
```typescript
const email = userInput;
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE email = '${email}'`
);
```
**VULNERABLE TO SQL INJECTION** - attacker can inject: `' OR '1'='1`
### 3. Use Prisma.sql for Dynamic Queries
```typescript
import { Prisma } from '@prisma/client';
const conditions: Prisma.Sql[] = [];
if (email) {
conditions.push(Prisma.sql`email = ${email}`);
}
if (status) {
conditions.push(Prisma.sql`status = ${status}`);
}
const where = conditions.length > 0
? Prisma.sql`WHERE ${Prisma.join(conditions, ' AND ')}`
: Prisma.empty;
const users = await prisma.$queryRaw`
SELECT * FROM "User" ${where}
`;
```
## Attack Vectors and Prevention
### Vector 1: String Concatenation in WHERE Clause
**VULNERABLE:**
```typescript
const searchTerm = req.query.search;
const results = await prisma.$queryRawUnsafe(
`SELECT * FROM "Product" WHERE name LIKE '%${searchTerm}%'`
);
```
**Attack:** `'; DELETE FROM "Product"; --`
**SAFE:**
```typescript
const searchTerm = req.query.search;
const results = await prisma.$queryRaw`
SELECT * FROM "Product" WHERE name LIKE ${'%' + searchTerm + '%'}
`;
```
### Vector 2: Dynamic Column Names
**VULNERABLE:**
```typescript
const sortColumn = req.query.sortBy;
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" ORDER BY ${sortColumn}`
);
```
**Attack:** `email; DROP TABLE "User"; --`
**SAFE:**
```typescript
const sortColumn = req.query.sortBy;
const allowedColumns = ['email', 'name', 'createdAt'];
if (!allowedColumns.includes(sortColumn)) {
throw new Error('Invalid sort column');
}
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" ORDER BY ${sortColumn}`
);
```
**Note:** Column names cannot be parameterized, so use allowlist validation.
### Vector 3: Dynamic Table Names
**VULNERABLE:**
```typescript
const tableName = req.params.table;
const data = await prisma.$queryRawUnsafe(
`SELECT * FROM "${tableName}"`
);
```
**Attack:** `User" WHERE 1=1; DROP TABLE "Session"; --`
**SAFE:**
```typescript
const tableName = req.params.table;
const allowedTables = ['User', 'Product', 'Order'];
if (!allowedTables.includes(tableName)) {
throw new Error('Invalid table name');
}
const data = await prisma.$queryRawUnsafe(
`SELECT * FROM "${tableName}"`
);
```
### Vector 4: IN Clause with Arrays
**VULNERABLE:**
```typescript
const ids = req.body.ids.join(',');
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE id IN (${ids})`
);
```
**Attack:** `1) OR 1=1; --`
**SAFE:**
```typescript
const ids = req.body.ids;
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE id IN (${Prisma.join(ids)})
`;
```
### Vector 5: LIMIT and OFFSET Injection
**VULNERABLE:**
```typescript
const limit = req.query.limit;
const offset = req.query.offset;
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" LIMIT ${limit} OFFSET ${offset}`
);
```
**Attack:** `10; DELETE FROM "User"; --`
**SAFE:**
```typescript
const limit = parseInt(req.query.limit, 10);
const offset = parseInt(req.query.offset, 10);
if (isNaN(limit) || isNaN(offset)) {
throw new Error('Invalid pagination parameters');
}
const users = await prisma.$queryRaw`
SELECT * FROM "User" LIMIT ${limit} OFFSET ${offset}
`;
```
## Dynamic Query Building Patterns
### Pattern 1: Optional Filters
```typescript
import { Prisma } from '@prisma/client';
interface SearchFilters {
email?: string;
status?: string;
minAge?: number;
}
async function searchUsers(filters: SearchFilters) {
const conditions: Prisma.Sql[] = [];
if (filters.email) {
conditions.push(Prisma.sql`email LIKE ${'%' + filters.email + '%'}`);
}
if (filters.status) {
conditions.push(Prisma.sql`status = ${filters.status}`);
}
if (filters.minAge !== undefined) {
conditions.push(Prisma.sql`age >= ${filters.minAge}`);
}
const where = conditions.length > 0
? Prisma.sql`WHERE ${Prisma.join(conditions, ' AND ')}`
: Prisma.empty;
return prisma.$queryRaw`
SELECT * FROM "User" ${where}
`;
}
```
### Pattern 2: Dynamic Sorting
```typescript
type SortColumn = 'email' | 'name' | 'createdAt';
type SortOrder = 'ASC' | 'DESC';
async function getUsers(sortBy: SortColumn, order: SortOrder) {
const allowedColumns: SortColumn[] = ['email', 'name', 'createdAt'];
const allowedOrders: SortOrder[] = ['ASC', 'DESC'];
if (!allowedColumns.includes(sortBy) || !allowedOrders.includes(order)) {
throw new Error('Invalid sort parameters');
}
return prisma.$queryRawUnsafe(
`SELECT * FROM "User" ORDER BY ${sortBy} ${order}`
);
}
```
### Pattern 3: Complex JOIN with Dynamic Conditions
```typescript
async function searchOrdersWithProducts(
userId?: number,
productName?: string,
minTotal?: number
) {
const conditions: Prisma.Sql[] = [];
if (userId !== undefined) {
conditions.push(Prisma.sql`o."userId" = ${userId}`);
}
if (productName) {
conditions.push(Prisma.sql`p.name LIKE ${'%' + productName + '%'}`);
}
if (minTotal !== undefined) {
conditions.push(Prisma.sql`o.total >= ${minTotal}`);
}
const where = conditions.length > 0
? Prisma.sql`WHERE ${Prisma.join(conditions, ' AND ')}`
: Prisma.empty;
return prisma.$queryRaw`
SELECT o.*, p.name as "productName"
FROM "Order" o
INNER JOIN "Product" p ON o."productId" = p.id
${where}
ORDER BY o."createdAt" DESC
`;
}
```
### Pattern 4: Batch Operations with Safe Arrays
```typescript
async function updateUserStatuses(
userIds: number[],
newStatus: string
) {
if (userIds.length === 0) {
return [];
}
return prisma.$queryRaw`
UPDATE "User"
SET status = ${newStatus}, "updatedAt" = NOW()
WHERE id IN (${Prisma.join(userIds)})
RETURNING *
`;
}
```
## When $queryRawUnsafe is Acceptable
`$queryRawUnsafe` is ONLY acceptable when:
1. **No user input involved** (static queries only)
2. **Identifiers from allowlist** (column/table names validated)
3. **Generated by type-safe builder** (internal tools, not user data)
```typescript
async function getTableSchema(tableName: 'User' | 'Product' | 'Order') {
return prisma.$queryRawUnsafe(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = '${tableName}'
`);
}
```
**Still requires:** TypeScript literal type or runtime validation against allowlist.
## Migration from $queryRawUnsafe
### Before:
```typescript
const status = req.query.status;
const minAge = req.query.minAge;
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE status = '${status}' AND age >= ${minAge}`
);
```
### After:
```typescript
const status = req.query.status;
const minAge = parseInt(req.query.minAge, 10);
const users = await prisma.$queryRaw`
SELECT * FROM "User"
WHERE status = ${status} AND age >= ${minAge}
`;
```
## Testing for SQL Injection
### Test Case 1: Authentication Bypass
```typescript
const maliciousEmail = "' OR '1'='1";
const user = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${maliciousEmail}
`;
```
**Expected:** Returns empty array (no match for literal string)
### Test Case 2: Comment Injection
```typescript
const maliciousInput = "test'; --";
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE name = ${maliciousInput}
`;
```
**Expected:** Searches for exact string `test'; --`, doesn't comment out rest of query
### Test Case 3: Union-Based Attack
```typescript
const maliciousId = "1 UNION SELECT password FROM Admin";
const user = await prisma.$queryRaw`
SELECT * FROM "User" WHERE id = ${maliciousId}
`;
```
**Expected:** Type error or no results (string cannot match integer id column)
## Detection and Remediation
### Detection Patterns
Use grep to find vulnerable code:
```bash
grep -r "\$queryRawUnsafe" --include="*.ts"
grep -r "queryRawUnsafe.*\${" --include="*.ts"
grep -r "queryRawUnsafe.*req\." --include="*.ts"
```
### Automated Detection
```typescript
import { ESLint } from 'eslint';
const dangerousPatterns = [
/\$queryRawUnsafe\s*\([^)]*\$\{/,
/queryRawUnsafe\s*\([^)]*req\./,
/queryRawUnsafe\s*\([^)]*params\./,
/queryRawUnsafe\s*\([^)]*query\./,
/queryRawUnsafe\s*\([^)]*body\./,
];
```
### Remediation Checklist
- [ ] Replace all `$queryRawUnsafe` with `$queryRaw` where user input exists
- [ ] Use `Prisma.sql` for dynamic query building
- [ ] Validate column/table names against allowlists
- [ ] Parameterize all user inputs
- [ ] Parse numeric inputs before use
- [ ] Use `Prisma.join()` for array parameters
- [ ] Add SQL injection test cases
- [ ] Run security audit tools
## Related Skills
**Security Best Practices:**
- If sanitizing user inputs before database operations, use the sanitizing-user-inputs skill from typescript for input sanitization patterns
## Resources
- [Prisma Raw Database Access Docs](https://www.prisma.io/docs/orm/prisma-client/queries/raw-database-access)
- [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
- [Prisma Security Best Practices](https://www.prisma.io/docs/orm/prisma-client/queries/raw-database-access#sql-injection)
## Summary
- **ALWAYS** use `$queryRaw` tagged templates for user input
- **NEVER** use `$queryRawUnsafe` with untrusted data
- **USE** `Prisma.sql` and `Prisma.join()` for dynamic queries
- **VALIDATE** column/table names against allowlists
- **TEST** for common SQL injection attack vectors
- **AUDIT** codebase regularly for `$queryRawUnsafe` usage

View File

@@ -0,0 +1,256 @@
---
name: reviewing-prisma-patterns
description: Review Prisma code for common violations, security issues, and performance anti-patterns found in AI coding agent stress testing. Use when reviewing Prisma Client usage, database operations, or performing code reviews on projects using Prisma ORM.
review: true
allowed-tools: Grep, Glob, Bash
version: 1.0.0
---
# Review Prisma Patterns
This skill performs systematic code review of Prisma usage, catching critical violations, security vulnerabilities, and performance anti-patterns identified through comprehensive stress testing of AI coding agents.
---
<role>
This skill systematically reviews Prisma codebases for 7 critical violation categories that cause production failures, security vulnerabilities, and performance degradation. Based on real-world failures found in 5 AI agents producing 30 violations during stress testing.
</role>
<when-to-activate>
This skill activates when:
- User requests code review of Prisma-based projects
- Performing security audit on database operations
- Investigating production issues (connection exhaustion, SQL injection, performance)
- Pre-deployment validation of Prisma code
- Working with files containing @prisma/client imports
</when-to-activate>
<overview>
The review checks for critical issues across 7 categories:
1. **Multiple PrismaClient Instances** (80% of agents failed)
2. **SQL Injection Vulnerabilities** (40% of agents failed)
3. **Missing Serverless Configuration** (60% of agents failed)
4. **Deprecated Buffer API** (Prisma 6 breaking change)
5. **Generic Error Handling** (Missing P-code checks)
6. **Missing Input Validation** (No Zod/schema validation)
7. **Inefficient Queries** (Offset pagination, missing select optimization)
Each violation includes severity rating, remediation steps, and reference to detailed Prisma 6 skills.
</overview>
<workflow>
## Standard Review Workflow
**Phase 1: Discovery**
1. Find all Prisma usage:
- Search for @prisma/client imports
- Identify PrismaClient instantiation
- Locate raw SQL operations
2. Identify project context:
- Check for serverless deployment (vercel.json, lambda/, app/ directory)
- Detect TypeScript vs JavaScript
- Find schema.prisma location
**Phase 2: Critical Issue Detection**
Run validation checks in order of severity:
1. **CRITICAL: SQL Injection** (P0 - Security vulnerability)
2. **CRITICAL: Multiple PrismaClient** (P0 - Connection exhaustion)
3. **HIGH: Serverless Misconfiguration** (P1 - Production failures)
4. **HIGH: Deprecated Buffer API** (P1 - Runtime errors)
5. **MEDIUM: Generic Error Handling** (P2 - Poor UX)
**Phase 3: Report Generation**
1. Group findings by severity
2. Provide file path + line number
3. Include code snippet
4. Reference remediation skill
5. Estimate impact (Low/Medium/High/Critical)
</workflow>
<validation-checks>
## Quick Check Summary
### P0 - CRITICAL (Must fix before deployment)
**1. SQL Injection Detection**
```bash
grep -rn "\$queryRawUnsafe\|Prisma\.raw" --include="*.ts" --include="*.js" .
```
Red flag: String concatenation with user input
Fix: Use `$queryRaw` tagged template
**2. Multiple PrismaClient Instances**
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" . | wc -l
```
Red flag: Count > 1
Fix: Global singleton pattern
### P1 - HIGH (Fix before production)
**3. Missing Serverless Configuration**
```bash
grep -rn "connection_limit=1" --include="*.env*" .
```
Red flag: No connection_limit in serverless app
Fix: Add `?connection_limit=1` to DATABASE_URL
**4. Deprecated Buffer API**
```bash
grep -rn "Buffer\.from" --include="*.ts" --include="*.js" . | grep -i "bytes"
```
Red flag: Buffer usage with Prisma Bytes fields
Fix: Use Uint8Array instead
See `references/validation-checks.md` for complete validation patterns with examples.
</validation-checks>
<review-workflow>
## Automated Review Process
**Step 1: Find Prisma Files**
```bash
find . -type f \( -name "*.ts" -o -name "*.js" \) -exec grep -l "@prisma/client" {} \;
```
**Step 2: Run All Checks**
Execute checks in severity order (P0 → P3):
1. SQL Injection check
2. Multiple PrismaClient check
3. Serverless configuration check
4. Deprecated Buffer API check
5. Error handling check
6. Input validation check
7. Query efficiency check
**Step 3: Generate Report**
Format:
```
Prisma Code Review - [Project Name]
Generated: [timestamp]
CRITICAL Issues (P0): [count]
HIGH Issues (P1): [count]
MEDIUM Issues (P2): [count]
LOW Issues (P3): [count]
---
[P0] SQL Injection Vulnerability
File: src/api/users.ts:45
Impact: CRITICAL - Enables SQL injection attacks
Fix: Use $queryRaw tagged template
Reference: @prisma-6/SECURITY-sql-injection
[P0] Multiple PrismaClient Instances
Files: src/db.ts:3, src/api/posts.ts:12
Count: 3 instances found
Impact: CRITICAL - Connection pool exhaustion
Fix: Use global singleton pattern
Reference: @prisma-6/CLIENT-singleton-pattern
```
</review-workflow>
<output-format>
## Report Format
Provide structured review with:
**Summary:**
- Total files reviewed
- Issues by severity (P0/P1/P2/P3)
- Overall assessment (Pass/Needs Fixes/Critical Issues)
**Detailed Findings:**
For each issue:
1. Severity badge ([P0] CRITICAL, [P1] HIGH, etc.)
2. Issue title
3. File path and line number
4. Code snippet (5 lines context)
5. Impact explanation
6. Specific remediation steps
7. Reference to detailed skill
**Remediation Priority:**
1. P0 issues must be fixed before deployment
2. P1 issues should be fixed before production
3. P2 issues improve code quality
4. P3 issues optimize performance
</output-format>
<constraints>
## Review Guidelines
**MUST:**
- Check all 7 critical issue categories
- Report findings with file path + line number
- Include code snippets for context
- Reference specific Prisma 6 skills for remediation
- Group by severity (P0 → P3)
**SHOULD:**
- Prioritize P0 (CRITICAL) issues first
- Provide specific fix recommendations
- Estimate impact of each violation
- Consider project context (serverless vs traditional)
**NEVER:**
- Skip P0 security checks
- Report false positives without verification
- Recommend fixes without testing patterns
- Ignore serverless-specific issues in serverless projects
</constraints>
<progressive-disclosure>
## Reference Files
For detailed information on specific topics:
- **Validation Checks**: See `references/validation-checks.md` for all 7 validation patterns with detailed examples
- **Example Reviews**: See `references/example-reviews.md` for complete review examples (e-commerce, dashboard)
Load references when performing deep review or encountering specific violation patterns.
</progressive-disclosure>
<validation>
## Review Validation
After generating review:
1. **Verify Findings:**
- Re-run grep commands to confirm matches
- Check context around flagged lines
- Eliminate false positives
2. **Test Remediation:**
- Verify recommended fixes are valid
- Ensure skill references are accurate
- Confirm impact assessments
3. **Completeness Check:**
- All 7 categories checked
- All Prisma files reviewed
- Severity correctly assigned
</validation>
---
**Integration:** This skill is discoverable by the review plugin via `review: true` frontmatter. Invoke with `/review prisma-patterns` or automatically when reviewing Prisma-based projects.
**Performance:** Review of typical project (50 files) completes in < 10 seconds using grep-based pattern matching.
**Updates:** As new Prisma violations emerge, add patterns to validation checks with corresponding skill references.

View File

@@ -0,0 +1,446 @@
# Example Reviews
Complete code review examples showing typical findings and recommendations.
---
## Example 1: E-commerce API (Next.js)
**Context:** Next.js 14 App Router with Prisma, deployed to Vercel
**Project Structure:**
```
app/
├── api/
│ ├── products/route.ts
│ ├── users/route.ts
│ └── search/route.ts
├── lib/
│ └── db.ts
└── .env
```
**Findings:**
```
Prisma Code Review - E-commerce API
Generated: 2025-11-21
Files Reviewed: 15
CRITICAL Issues (P0): 2
HIGH Issues (P1): 1
MEDIUM Issues (P2): 3
LOW Issues (P3): 2
Overall Assessment: CRITICAL ISSUES - Do not deploy
---
[P0] Multiple PrismaClient Instances
Files:
- app/api/products/route.ts:8
- app/api/users/route.ts:12
- lib/db.ts:5
Count: 3 instances found
Code (app/api/products/route.ts:8):
```typescript
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET() {
const products = await prisma.product.findMany()
return Response.json(products)
}
```
Impact: CRITICAL - Connection pool exhaustion under load
- Each API route creates separate connection pool
- Vercel scales to 100+ concurrent functions
- 3 routes × 100 instances × 10 connections = 3000 connections!
- Database will reject connections (P1017)
Fix: Create global singleton in lib/db.ts, import everywhere
Remediation Steps:
1. Create lib/prisma.ts with global singleton pattern
2. Replace all `new PrismaClient()` with imports
3. Verify with grep (should find only 1 instance)
Reference: @prisma-6/CLIENT-singleton-pattern
---
[P0] SQL Injection Vulnerability
File: app/api/search/route.ts:23
Code:
```typescript
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')
const products = await prisma.$queryRawUnsafe(
`SELECT * FROM products WHERE name LIKE '%${query}%'`
)
return Response.json(products)
}
```
Impact: CRITICAL - Enables SQL injection attacks
- User controls `query` parameter
- Direct string interpolation allows injection
- Attacker can execute arbitrary SQL
Example attack:
```
/api/search?q=%27;%20DROP%20TABLE%20products;--
```
Fix: Use $queryRaw tagged template with automatic parameterization
Remediation:
```typescript
const products = await prisma.$queryRaw`
SELECT * FROM products WHERE name LIKE ${'%' + query + '%'}
`
```
Reference: @prisma-6/SECURITY-sql-injection
---
[P1] Missing Serverless Configuration
File: .env
Current:
```
DATABASE_URL="postgresql://user:pass@host:5432/db"
```
Impact: HIGH - Connection exhaustion in Vercel deployment
- Default pool_size = 10 connections per instance
- Vercel can scale to 100+ instances
- 100 instances × 10 connections = 1000 connections
- Most databases have 100-200 connection limit
Fix: Add ?connection_limit=1 to DATABASE_URL
Remediation:
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10"
```
Why this works:
- Each Vercel function instance gets 1 connection
- 100 instances × 1 connection = 100 connections (sustainable)
- pool_timeout=10 prevents hanging on exhaustion
Reference: @prisma-6/CLIENT-serverless-config
---
[P2] Missing Input Validation
Files: app/api/users/route.ts, app/api/products/route.ts
Code (app/api/users/route.ts):
```typescript
export async function POST(request: Request) {
const data = await request.json()
const user = await prisma.user.create({ data })
return Response.json(user)
}
```
Impact: MEDIUM - Invalid data can reach database
- No validation of email format
- No validation of required fields
- Type mismatches cause runtime errors
Fix: Add Zod validation schemas before Prisma operations
Remediation:
```typescript
import { z } from 'zod'
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
})
export async function POST(request: Request) {
const data = await request.json()
const validated = userSchema.parse(data)
const user = await prisma.user.create({ data: validated })
return Response.json(user)
}
```
Reference: @prisma-6/SECURITY-input-validation
---
[P3] Inefficient Pagination
File: app/api/products/route.ts:15
Code:
```typescript
const page = parseInt(searchParams.get('page') ?? '0')
const products = await prisma.product.findMany({
skip: page * 100,
take: 100
})
```
Impact: LOW - Slow queries on large datasets
- Product table has 50k+ records
- Offset pagination degrades with page number
- Page 500 skips 50k records (slow!)
Fix: Use cursor-based pagination with id cursor
Remediation:
```typescript
const cursor = searchParams.get('cursor')
const products = await prisma.product.findMany({
take: 100,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { id: 'asc' }
})
const nextCursor = products.length === 100
? products[99].id
: null
```
Reference: @prisma-6/QUERIES-pagination
---
RECOMMENDATION: Fix P0 issues immediately before any deployment. P1 issues will cause production failures under load.
Priority Actions:
1. Implement global singleton pattern (blocking)
2. Fix SQL injection in search endpoint (blocking)
3. Add connection_limit to DATABASE_URL (high priority)
4. Add Zod validation to API routes (recommended)
5. Optimize pagination for products (nice to have)
```
---
## Example 2: Internal Dashboard (Express)
**Context:** Express API with PostgreSQL, traditional server deployment
**Project Structure:**
```
src/
├── controllers/
│ ├── users.ts
│ └── reports.ts
├── db.ts
└── index.ts
```
**Findings:**
```
Prisma Code Review - Internal Dashboard
Generated: 2025-11-21
Files Reviewed: 8
CRITICAL Issues (P0): 0
HIGH Issues (P1): 0
MEDIUM Issues (P2): 1
LOW Issues (P3): 3
Overall Assessment: GOOD - Minor improvements recommended
---
[P2] Generic Error Handling
File: src/controllers/users.ts:45-52
Code:
```typescript
async function createUser(req: Request, res: Response) {
try {
const user = await prisma.user.create({
data: req.body
})
res.json(user)
} catch (error) {
res.status(500).json({ error: 'Database error' })
}
}
```
Impact: MEDIUM - P2002/P2025 not handled specifically
- User gets generic "Database error" for all failures
- Duplicate email returns 500 instead of 409
- Poor developer experience debugging issues
Fix: Check error.code for P2002 (unique), P2025 (not found)
Remediation:
```typescript
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
async function createUser(req: Request, res: Response) {
try {
const user = await prisma.user.create({
data: req.body
})
res.json(user)
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return res.status(409).json({
error: 'User with this email already exists'
})
}
}
res.status(500).json({ error: 'Unexpected error' })
}
}
```
Reference: @prisma-6/TRANSACTIONS-error-handling
---
[P3] Inefficient Pagination
File: src/controllers/reports.ts:78
Code:
```typescript
const reports = await prisma.report.findMany({
skip: page * 100,
take: 100,
orderBy: { createdAt: 'desc' }
})
```
Context:
- Reports table has 50k+ records
- Used in admin dashboard for audit logs
- Page 500 requires scanning 50k records
Impact: LOW - Slow queries on large datasets
- Query time increases with page number
- Database performs full table scan
- Admin dashboard feels sluggish
Fix: Use cursor-based pagination with id cursor
Remediation:
```typescript
const reports = await prisma.report.findMany({
take: 100,
cursor: lastId ? { id: lastId } : undefined,
orderBy: { createdAt: 'desc' }
})
```
Reference: @prisma-6/QUERIES-pagination
---
[P3] Missing Select Optimization
Files: 8 files with findMany() lacking select
Examples:
- src/controllers/users.ts:23
- src/controllers/reports.ts:45
- src/controllers/analytics.ts:67
Code pattern:
```typescript
const users = await prisma.user.findMany()
```
Impact: LOW - Fetching unnecessary fields
- Returns all columns including large text fields
- Increases response payload size
- Wastes database bandwidth
Fix: Add select: { id, name, email } to queries
Remediation:
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
role: true
}
})
```
Reference: @prisma-6/QUERIES-select-optimization
---
[P3] Missing Select in List Endpoints
File: src/controllers/users.ts:88
Code:
```typescript
const users = await prisma.user.findMany({
include: { posts: true }
})
```
Impact: LOW - Over-fetching related data
- Returns ALL posts for each user
- User with 1000 posts = huge payload
- Should paginate posts separately
Fix: Limit included records or use separate query
Remediation:
```typescript
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
name: true,
_count: {
select: { posts: true }
}
}
})
```
Reference: @prisma-6/QUERIES-select-optimization
---
ASSESSMENT: Code quality is good. No critical issues found.
Recommended Improvements:
1. Improve error handling with P-code checks (user experience)
2. Optimize pagination for reports table (performance)
3. Add select clauses to list endpoints (efficiency)
These improvements are optional but will enhance code quality and performance.
```
---
## Summary
**E-commerce API:**
- High-risk serverless deployment with critical security/stability issues
- Must fix P0 issues before deployment
- Typical of AI-generated code without production hardening
**Internal Dashboard:**
- Low-risk traditional server deployment with minor optimizations
- No blocking issues
- Good baseline quality with room for improvement
Both examples demonstrate the importance of systematic code review before production deployment.

View File

@@ -0,0 +1,433 @@
# Validation Checks
Complete validation patterns for all 7 critical issue categories in Prisma code review.
---
## 1. SQL Injection Detection (CRITICAL - P0)
**Pattern:** Unsafe raw SQL usage
**Detection Command:**
```bash
grep -rn "\$queryRawUnsafe\|Prisma\.raw" --include="*.ts" --include="*.js" .
```
**Red flags:**
- `$queryRawUnsafe` with string concatenation
- `Prisma.raw()` with template literals (non-tagged)
- Dynamic table/column names via string interpolation
- Filter conditions with user input interpolation
**Example violations:**
```typescript
const users = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${email}'`
);
const posts = await prisma.$queryRaw(
Prisma.raw(`SELECT * FROM posts WHERE title LIKE '%${search}%'`)
);
```
**Remediation:**
Use `$queryRaw` tagged template for automatic parameterization:
```typescript
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
const posts = await prisma.$queryRaw`
SELECT * FROM posts WHERE title LIKE ${'%' + search + '%'}
`;
```
Use Prisma.sql for composition:
```typescript
import { Prisma } from '@prisma/client'
const emailFilter = Prisma.sql`email = ${email}`
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE ${emailFilter}
`
```
**Impact:** CRITICAL - SQL injection enables arbitrary database access, data exfiltration, deletion
**Reference:** @prisma-6/SECURITY-sql-injection
---
## 2. Multiple PrismaClient Instances (CRITICAL - P0)
**Pattern:** Multiple client instantiation
**Detection Command:**
```bash
grep -rn "new PrismaClient()" --include="*.ts" --include="*.js" . | wc -l
```
**Red flags:**
- Count > 1 across codebase
- Function-scoped client creation
- Missing global singleton pattern
- Test files creating separate instances
**Example violations:**
```typescript
export function getUser(id: string) {
const prisma = new PrismaClient();
return prisma.user.findUnique({ where: { id } });
}
export function getPost(id: string) {
const prisma = new PrismaClient();
return prisma.post.findUnique({ where: { id } });
}
```
**Remediation:**
Create global singleton:
```typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
Import singleton everywhere:
```typescript
import { prisma } from '@/lib/prisma'
export function getUser(id: string) {
return prisma.user.findUnique({ where: { id } });
}
```
**Impact:** CRITICAL - Connection pool exhaustion, P1017 errors, production outages
**Reference:** @prisma-6/CLIENT-singleton-pattern
---
## 3. Missing Serverless Configuration (HIGH - P1)
**Pattern:** Serverless deployment without connection limits
**Detection:**
1. Check for serverless context:
```bash
test -f vercel.json || test -d app/ || grep -q "lambda" package.json
```
2. Check for connection_limit:
```bash
grep -rn "connection_limit=1" --include="*.env*" --include="schema.prisma" .
```
**Red flags:**
- Serverless deployment detected (Vercel, Lambda, Cloudflare Workers)
- No `connection_limit=1` in DATABASE_URL
- No PgBouncer configuration
- Default pool_timeout settings
**Example violation:**
```
DATABASE_URL="postgresql://user:pass@host:5432/db"
```
**Remediation:**
Add connection_limit to DATABASE_URL:
```
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=1&pool_timeout=10"
```
For Next.js on Vercel:
```typescript
export const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL + '?connection_limit=1'
}
}
})
```
**Impact:** HIGH - Production database connection exhaustion under load
**Reference:** @prisma-6/CLIENT-serverless-config
---
## 4. Deprecated Buffer API (HIGH - P1)
**Pattern:** Prisma 6 breaking change - Buffer on Bytes fields
**Detection Command:**
```bash
grep -rn "Buffer\.from\|\.toString()" --include="*.ts" --include="*.js" . | grep -i "bytes\|binary"
```
**Red flags:**
- `Buffer.from()` used with Bytes fields
- `.toString()` called on Bytes field results
- Missing Uint8Array conversion
- Missing TextEncoder/TextDecoder
**Example violations:**
```typescript
const user = await prisma.user.create({
data: {
avatar: Buffer.from(base64Data, 'base64')
}
});
const avatarString = user.avatar.toString('base64');
```
**Remediation:**
Use Uint8Array instead of Buffer:
```typescript
const base64ToUint8Array = (base64: string) => {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
};
const user = await prisma.user.create({
data: {
avatar: base64ToUint8Array(base64Data)
}
});
```
Use TextEncoder/TextDecoder:
```typescript
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const user = await prisma.user.create({
data: {
content: encoder.encode('Hello')
}
});
const text = decoder.decode(user.content);
```
**Impact:** HIGH - Type errors, runtime failures after Prisma 6 upgrade
**Reference:** @prisma-6/MIGRATIONS-v6-upgrade
---
## 5. Generic Error Handling (MEDIUM - P2)
**Pattern:** Missing Prisma error code handling
**Detection Command:**
```bash
grep -rn "catch.*error" --include="*.ts" --include="*.js" . | grep -L "P2002\|P2025\|PrismaClientKnownRequestError"
```
**Red flags:**
- Generic `catch (error)` without P-code checking
- No differentiation between error types
- Exposing raw Prisma errors to clients
- Missing unique constraint handling (P2002)
- Missing not found handling (P2025)
**Example violation:**
```typescript
try {
await prisma.user.create({ data });
} catch (error) {
throw new Error('Database error');
}
```
**Remediation:**
Check error.code for specific P-codes:
```typescript
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
try {
await prisma.user.create({ data })
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new Error('User with this email already exists')
}
if (error.code === 'P2025') {
throw new Error('Record not found')
}
}
throw new Error('Unexpected error')
}
```
**Impact:** MEDIUM - Poor user experience, unclear error messages, potential info leakage
**Reference:** @prisma-6/TRANSACTIONS-error-handling, @prisma-6/SECURITY-error-exposure
---
## 6. Missing Input Validation (MEDIUM - P2)
**Pattern:** No validation before database operations
**Detection Command:**
```bash
grep -rn "prisma\.\w+\.(create\|update\|upsert)" --include="*.ts" --include="*.js" . | grep -L "parse\|validate\|schema"
```
**Red flags:**
- Direct database operations with external input
- No Zod/Yup/Joi schema validation
- Type assertions without runtime checks
- Missing email/phone/URL validation
**Example violation:**
```typescript
export async function createUser(data: any) {
return prisma.user.create({ data });
}
```
**Remediation:**
Add Zod schema validation:
```typescript
import { z } from 'zod'
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
age: z.number().int().positive().optional()
})
export async function createUser(data: unknown) {
const validated = userSchema.parse(data)
return prisma.user.create({ data: validated })
}
```
**Impact:** MEDIUM - Type mismatches, invalid data in database, runtime errors
**Reference:** @prisma-6/SECURITY-input-validation
---
## 7. Inefficient Queries (LOW - P3)
**Pattern:** Performance anti-patterns
**Detection Commands:**
```bash
grep -rn "\.skip\|\.take" --include="*.ts" --include="*.js" .
grep -rn "prisma\.\w+\.findMany()" --include="*.ts" --include="*.js" . | grep -v "select\|include"
```
**Red flags:**
- Offset pagination (skip/take) on large datasets (> 10k records)
- Missing `select` for partial queries
- N+1 queries (findMany in loops without include)
- Missing indexes for frequent queries
**Example violations:**
Offset pagination on large dataset:
```typescript
const users = await prisma.user.findMany({
skip: page * 100,
take: 100
});
```
Missing select optimization:
```typescript
const users = await prisma.user.findMany();
```
N+1 query:
```typescript
const users = await prisma.user.findMany();
for (const user of users) {
const posts = await prisma.post.findMany({
where: { authorId: user.id }
});
}
```
**Remediation:**
Use cursor-based pagination:
```typescript
const users = await prisma.user.findMany({
take: 100,
cursor: lastId ? { id: lastId } : undefined,
orderBy: { id: 'asc' }
});
```
Add select for partial queries:
```typescript
const users = await prisma.user.findMany({
select: { id: true, email: true, name: true }
});
```
Fix N+1 with include:
```typescript
const users = await prisma.user.findMany({
include: { posts: true }
});
```
**Impact:** LOW - Slow queries, high database load, poor performance at scale
**Reference:** @prisma-6/QUERIES-pagination, @prisma-6/QUERIES-select-optimization
---
## Summary Table
| Check | Severity | Detection | Common Fix | Skill Reference |
|-------|----------|-----------|------------|-----------------|
| SQL Injection | P0 | `$queryRawUnsafe` | Use `$queryRaw` tagged template | SECURITY-sql-injection |
| Multiple Clients | P0 | Count `new PrismaClient()` | Global singleton pattern | CLIENT-singleton-pattern |
| Serverless Config | P1 | Missing `connection_limit` | Add `?connection_limit=1` | CLIENT-serverless-config |
| Buffer API | P1 | `Buffer.from` with Bytes | Use Uint8Array | MIGRATIONS-v6-upgrade |
| Error Handling | P2 | Generic catch | Check P-codes (P2002, P2025) | TRANSACTIONS-error-handling |
| Input Validation | P2 | No validation before DB | Add Zod schema | SECURITY-input-validation |
| Query Efficiency | P3 | skip/take, no select | Cursor pagination, select | QUERIES-pagination |

View File

@@ -0,0 +1,194 @@
---
name: upgrading-to-prisma-6
description: Migrate from Prisma 5 to Prisma 6 handling breaking changes including Buffer to Uint8Array, implicit m-n PK changes, NotFoundError to P2025, and reserved keywords. Use when upgrading Prisma, encountering Prisma 6 type errors, or migrating legacy code.
allowed-tools: Read, Write, Edit, Grep, Glob
version: 1.0.0
---
# Prisma 6 Migration Guide
This skill guides you through upgrading from Prisma 5 to Prisma 6, handling all breaking changes systematically to prevent runtime failures and type errors.
---
<role>
This skill teaches Claude how to migrate Prisma 5 codebases to Prisma 6 following the official migration guide, addressing breaking changes in Buffer API, implicit many-to-many relationships, error handling, and reserved keywords.
</role>
<when-to-activate>
This skill activates when:
- User mentions "Prisma 6", "upgrade Prisma", "migrate to Prisma 6"
- Encountering Prisma 6 type errors related to Bytes fields
- Working with Prisma migrations or schema changes during upgrades
- User reports NotFoundError issues after upgrading
- Reserved keyword conflicts appear (`async`, `await`, `using`)
</when-to-activate>
<overview>
Prisma 6 introduces four critical breaking changes that require code updates:
1. **Buffer → Uint8Array**: Bytes fields now use Uint8Array instead of Buffer
2. **Implicit m-n PKs**: Many-to-many join tables now use compound primary keys
3. **NotFoundError → P2025**: Error class removed, use error code checking
4. **Reserved Keywords**: `async`, `await`, `using` are now reserved model/field names
Attempting to use Prisma 6 without these updates causes type errors, runtime failures, and migration issues.
</overview>
<workflow>
## Migration Workflow
**Phase 1: Pre-Migration Assessment**
1. Identify all Bytes fields in schema
- Use Grep to find `@db.ByteA`, `Bytes` field types
- List all files using Buffer operations on Bytes fields
2. Find implicit many-to-many relationships
- Search schema for relation fields without explicit join tables
- Identify models with `@relation` without `relationName`
3. Locate NotFoundError usage
- Grep for `NotFoundError` imports and usage
- Find error handling that checks error class
4. Check for reserved keywords
- Search schema for models/fields named `async`, `await`, `using`
**Phase 2: Schema Migration**
1. Update reserved keywords in schema
- Rename any models/fields using reserved words
- Update all references in application code
2. Generate migration for implicit m-n changes
- Run `npx prisma migrate dev --name v6-implicit-mn-pks`
- Review generated SQL for compound primary key changes
**Phase 3: Code Migration**
1. Update Buffer → Uint8Array conversions
- Replace `Buffer.from()` with TextEncoder
- Replace `.toString()` with TextDecoder
- Update type annotations from Buffer to Uint8Array
2. Update NotFoundError handling
- Replace error class checks with P2025 code checks
- Use `isPrismaClientKnownRequestError` type guard
3. Test all changes
- Run existing tests
- Verify Bytes field operations
- Confirm error handling works correctly
**Phase 4: Validation**
1. Run TypeScript compiler
- Verify no type errors remain
- Check all Buffer references resolved
2. Run database migrations
- Apply migrations to test database
- Verify compound PKs created correctly
3. Runtime testing
- Test Bytes field read/write operations
- Verify error handling catches not-found cases
- Confirm implicit m-n queries work
</workflow>
## Quick Reference
**Breaking Changes Summary:**
| Change | Before | After |
|--------|--------|-------|
| Buffer API | `Buffer.from()`, `.toString()` | `TextEncoder`, `TextDecoder` |
| Error Handling | `error instanceof NotFoundError` | `error.code === 'P2025'` |
| Implicit m-n PK | Auto-increment `id` | Compound PK `(A, B)` |
| Reserved Words | `async`, `await`, `using` allowed | Must use `@map()` |
**Migration Command:**
```bash
npx prisma migrate dev --name v6-upgrade
```
**Validation Commands:**
```bash
npx tsc --noEmit
npx prisma migrate status
npm test
```
<constraints>
## Migration Guidelines
**MUST:**
- Backup production database before migration
- Test migration in development/staging first
- Review auto-generated migration SQL
- Update all Buffer operations to TextEncoder/TextDecoder
- Replace all NotFoundError checks with P2025 code checks
- Run TypeScript compiler to verify no type errors
**SHOULD:**
- Create helper functions for common error checks
- Use `@map()` when renaming reserved keywords
- Document breaking changes in commit messages
- Update team documentation about Prisma 6 patterns
**NEVER:**
- Run migrations directly in production without testing
- Skip TypeScript compilation check
- Leave Buffer references in code (causes type errors)
- Use NotFoundError (removed in Prisma 6)
- Use `async`, `await`, `using` as model/field names without `@map()`
</constraints>
<validation>
## Post-Migration Validation
After completing migration:
1. **TypeScript Compilation:**
- Run: `npx tsc --noEmit`
- Expected: Zero type errors
- If fails: Check remaining Buffer references, NotFoundError usage
2. **Database Migration Status:**
- Run: `npx prisma migrate status`
- Expected: All migrations applied
- If fails: Apply pending migrations with `npx prisma migrate deploy`
3. **Runtime Testing:**
- Test Bytes field write/read cycle
- Verify error handling catches P2025 correctly
- Test implicit m-n relationship queries
- Confirm no runtime errors in production-like environment
4. **Performance Check:**
- Verify query performance unchanged
- Check connection pool behavior
- Monitor error rates in logs
5. **Rollback Readiness:**
- Document rollback steps
- Keep Prisma 5 migration snapshot
- Test rollback procedure in staging
</validation>
## References
For detailed migration guides and examples:
- **Breaking Changes Details**: See `references/breaking-changes.md` for complete API migration patterns, SQL examples, and edge cases
- **Migration Examples**: See `references/migration-examples.md` for real-world migration scenarios with before/after code
- **Migration Checklist**: See `references/migration-checklist.md` for step-by-step migration tasks
- **Troubleshooting Guide**: See `references/troubleshooting.md` for common migration issues and solutions
For framework-specific migration patterns:
- **Next.js Integration**: Consult Next.js plugin for App Router-specific Prisma 6 patterns
- **Serverless Deployment**: See CLIENT-serverless-config skill for Prisma 6 + Lambda/Vercel
For error handling patterns:
- **Error Code Reference**: See TRANSACTIONS-error-handling skill for comprehensive P-code handling

View File

@@ -0,0 +1,173 @@
# Prisma 6 Breaking Changes - Detailed Reference
## 1. Buffer → Uint8Array
**Before (Prisma 5):**
```typescript
const user = await prisma.user.create({
data: {
name: 'Alice',
data: Buffer.from('hello', 'utf-8')
}
})
const text = user.data.toString('utf-8')
```
**After (Prisma 6):**
```typescript
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const user = await prisma.user.create({
data: {
name: 'Alice',
data: encoder.encode('hello')
}
})
const text = decoder.decode(user.data)
```
**Type Changes:**
- Schema `Bytes` type now maps to `Uint8Array` instead of `Buffer`
- All database binary data returned as `Uint8Array`
- `Buffer` methods no longer available on Bytes fields
**Migration Steps:**
1. Find all Buffer operations: `grep -r "Buffer.from\|\.toString(" --include="*.ts" --include="*.js"`
2. Replace with TextEncoder/TextDecoder
3. Update type annotations: `Buffer``Uint8Array`
## 2. Implicit Many-to-Many Primary Keys
**Before (Prisma 5):**
Implicit m-n join tables had auto-generated integer primary keys.
**After (Prisma 6):**
Implicit m-n join tables use compound primary keys based on foreign keys.
**Example Schema:**
```prisma
model Post {
id Int @id @default(autoincrement())
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
posts Post[]
}
```
**Migration Impact:**
- Prisma generates `_CategoryToPost` join table
- **Prisma 5**: PK was auto-increment `id`
- **Prisma 6**: PK is compound `(A, B)` where A/B are foreign keys
**Migration:**
```sql
ALTER TABLE "_CategoryToPost" DROP CONSTRAINT "_CategoryToPost_pkey";
ALTER TABLE "_CategoryToPost" ADD CONSTRAINT "_CategoryToPost_AB_pkey" PRIMARY KEY ("A", "B");
```
This migration is auto-generated when running `prisma migrate dev` after upgrading.
**Action Required:**
- Run migration in development
- Review generated SQL before production deploy
- No code changes needed (Prisma Client handles internally)
## 3. NotFoundError → P2025 Error Code
**Before (Prisma 5):**
```typescript
import { PrismaClient, NotFoundError } from '@prisma/client'
try {
const user = await prisma.user.delete({
where: { id: 999 }
})
} catch (error) {
if (error instanceof NotFoundError) {
console.log('User not found')
}
}
```
**After (Prisma 6):**
```typescript
import { PrismaClient, Prisma } from '@prisma/client'
try {
const user = await prisma.user.delete({
where: { id: 999 }
})
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2025') {
console.log('User not found')
}
}
}
```
**Type Guard Pattern:**
```typescript
function isNotFoundError(error: unknown): boolean {
return (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
)
}
try {
const user = await prisma.user.delete({ where: { id: 999 } })
} catch (error) {
if (isNotFoundError(error)) {
console.log('User not found')
}
throw error
}
```
**Migration Steps:**
1. Find all NotFoundError usage: `grep -r "NotFoundError" --include="*.ts"`
2. Remove NotFoundError imports
3. Replace error class checks with P2025 code checks
4. Use `Prisma.PrismaClientKnownRequestError` type guard
## 4. Reserved Keywords
**Breaking Change:**
The following field/model names are now reserved:
- `async`
- `await`
- `using`
**Before (Prisma 5):**
```prisma
model Task {
id Int @id @default(autoincrement())
async Boolean
}
```
**After (Prisma 6):**
```prisma
model Task {
id Int @id @default(autoincrement())
isAsync Boolean @map("async")
}
```
**Migration Steps:**
1. Find reserved keywords in schema: `grep -E "^\s*(async|await|using)\s" schema.prisma`
2. Rename fields/models with descriptive alternatives
3. Use `@map()` to maintain database column names
4. Update all application code references
**Recommended Renames:**
- `async``isAsync`, `asyncMode`, `asynchronous`
- `await``awaitStatus`, `pending`, `waitingFor`
- `using``inUse`, `isActive`, `usage`

View File

@@ -0,0 +1,72 @@
# Prisma 6 Migration Checklist
## Pre-Migration
- [ ] Backup production database
- [ ] Create feature branch for migration
- [ ] Run existing tests to establish baseline
- [ ] Document current Prisma version
## Schema Assessment
- [ ] Search for Bytes fields: `grep "Bytes" prisma/schema.prisma`
- [ ] Search for implicit m-n relations (no explicit join table)
- [ ] Search for reserved keywords: `grep -E "^\s*(async|await|using)\s" prisma/schema.prisma`
- [ ] List all models and relations
## Code Assessment
- [ ] Find Buffer usage: `grep -r "Buffer\\.from\\|Buffer\\.alloc" --include="*.ts"`
- [ ] Find toString on Bytes: `grep -r "\\.toString(" --include="*.ts"`
- [ ] Find NotFoundError: `grep -r "NotFoundError" --include="*.ts"`
- [ ] Document all locations requiring changes
## Update Dependencies
- [ ] Update package.json: `npm install prisma@6 @prisma/client@6`
- [ ] Regenerate client: `npx prisma generate`
- [ ] Verify TypeScript errors appear (expected)
## Schema Migration
- [ ] Rename any reserved keyword fields/models
- [ ] Add `@map()` to maintain database compatibility
- [ ] Run `npx prisma migrate dev --name v6-upgrade`
- [ ] Review generated migration SQL
- [ ] Test migration on development database
## Code Updates: Buffer → Uint8Array
- [ ] Create TextEncoder/TextDecoder instances
- [ ] Replace `Buffer.from(str, 'utf-8')` with `encoder.encode(str)`
- [ ] Replace `buffer.toString('utf-8')` with `decoder.decode(uint8array)`
- [ ] Update type annotations: `Buffer``Uint8Array`
- [ ] Handle edge cases (binary data, non-UTF8 encodings)
## Code Updates: NotFoundError → P2025
- [ ] Remove `NotFoundError` imports
- [ ] Replace `error instanceof NotFoundError` with P2025 checks
- [ ] Import `Prisma` from '@prisma/client'
- [ ] Use `Prisma.PrismaClientKnownRequestError` type guard
- [ ] Create helper functions for common error checks
## Testing
- [ ] Run TypeScript compiler: `npx tsc --noEmit`
- [ ] Fix any remaining type errors
- [ ] Run unit tests
- [ ] Run integration tests
- [ ] Test Bytes field operations manually
- [ ] Test not-found error handling
- [ ] Test implicit m-n queries
## Production Deployment
- [ ] Review migration SQL one final time
- [ ] Plan maintenance window if needed
- [ ] Deploy migration: `npx prisma migrate deploy`
- [ ] Deploy application code
- [ ] Monitor error logs for issues
- [ ] Verify Bytes operations work correctly
- [ ] Rollback plan ready if needed

View File

@@ -0,0 +1,193 @@
# Prisma 6 Migration Examples
## Example 1: Complete Bytes Field Migration
**Schema:**
```prisma
model Document {
id Int @id @default(autoincrement())
content Bytes
}
```
**Before (Prisma 5):**
```typescript
const doc = await prisma.document.create({
data: {
content: Buffer.from('Important document content', 'utf-8')
}
})
console.log(doc.content.toString('utf-8'))
```
**After (Prisma 6):**
```typescript
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const doc = await prisma.document.create({
data: {
content: encoder.encode('Important document content')
}
})
console.log(decoder.decode(doc.content))
```
**Binary Data (non-text):**
```typescript
const binaryData = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])
const doc = await prisma.document.create({
data: {
content: binaryData
}
})
const retrieved = await prisma.document.findUnique({ where: { id: doc.id } })
console.log(retrieved.content)
```
## Example 2: NotFoundError Migration
**Before (Prisma 5):**
```typescript
import { PrismaClient, NotFoundError } from '@prisma/client'
async function deleteUser(id: number) {
try {
const user = await prisma.user.delete({ where: { id } })
return { success: true, user }
} catch (error) {
if (error instanceof NotFoundError) {
return { success: false, error: 'User not found' }
}
throw error
}
}
```
**After (Prisma 6):**
```typescript
import { PrismaClient, Prisma } from '@prisma/client'
async function deleteUser(id: number) {
try {
const user = await prisma.user.delete({ where: { id } })
return { success: true, user }
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2025') {
return { success: false, error: 'User not found' }
}
}
throw error
}
}
```
**Reusable Helper:**
```typescript
import { Prisma } from '@prisma/client'
export function isPrismaNotFoundError(
error: unknown
): error is Prisma.PrismaClientKnownRequestError {
return (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2025'
)
}
async function deleteUser(id: number) {
try {
const user = await prisma.user.delete({ where: { id } })
return { success: true, user }
} catch (error) {
if (isPrismaNotFoundError(error)) {
return { success: false, error: 'User not found' }
}
throw error
}
}
```
## Example 3: Reserved Keyword Migration
**Before (Prisma 5):**
```prisma
model Task {
id Int @id @default(autoincrement())
async Boolean
await String?
}
```
**After (Prisma 6):**
```prisma
model Task {
id Int @id @default(autoincrement())
isAsync Boolean @map("async")
awaitMsg String? @map("await")
}
```
**Code Update:**
```typescript
const task = await prisma.task.create({
data: {
isAsync: true,
awaitMsg: 'Waiting for completion'
}
})
console.log(task.isAsync)
console.log(task.awaitMsg)
```
**Database columns remain unchanged** (`async`, `await`), but TypeScript code uses new names.
## Example 4: Implicit Many-to-Many Migration
**Schema:**
```prisma
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
```
**Auto-Generated Migration:**
```sql
ALTER TABLE "_CategoryToPost" DROP CONSTRAINT "_CategoryToPost_pkey";
ALTER TABLE "_CategoryToPost" ADD CONSTRAINT "_CategoryToPost_AB_pkey"
PRIMARY KEY ("A", "B");
```
**No code changes needed**:
```typescript
const post = await prisma.post.create({
data: {
title: 'Hello World',
categories: {
connect: [{ id: 1 }, { id: 2 }]
}
}
})
const postWithCategories = await prisma.post.findUnique({
where: { id: post.id },
include: { categories: true }
})
```
**Migration runs automatically** when you run `npx prisma migrate dev` after upgrading to Prisma 6.

View File

@@ -0,0 +1,45 @@
# Prisma 6 Migration Troubleshooting
## Issue: Type error on Bytes field
**Error:**
```
Type 'Buffer' is not assignable to type 'Uint8Array'
```
**Solution:**
Replace Buffer operations with TextEncoder/TextDecoder or use Uint8Array directly.
## Issue: Migration fails with duplicate key
**Error:**
```
ERROR: duplicate key value violates unique constraint "_CategoryToPost_AB_unique"
```
**Solution:**
Implicit m-n tables may have duplicate entries. Clean data before migration:
```sql
DELETE FROM "_CategoryToPost" a USING "_CategoryToPost" b
WHERE a.ctid < b.ctid AND a."A" = b."A" AND a."B" = b."B";
```
## Issue: NotFoundError import fails
**Error:**
```
Module '"@prisma/client"' has no exported member 'NotFoundError'
```
**Solution:**
Remove NotFoundError import, use P2025 error code checking instead.
## Issue: Reserved keyword compilation error
**Error:**
```
'async' is a reserved word
```
**Solution:**
Rename field in schema with `@map()` to preserve database column name.

View File

@@ -0,0 +1,305 @@
---
name: using-interactive-transactions
description: Use interactive transactions with $transaction callback for atomic operations and automatic rollback. Use when operations must succeed or fail together.
allowed-tools:
- Read
- Write
- Edit
---
# Interactive Transactions
Use the `$transaction` callback API for operations that must succeed or fail atomically. Interactive transactions provide automatic rollback on errors and allow complex multi-step logic.
## When to Use
Use interactive transactions when:
- Multiple operations must all succeed or all fail
- Operations depend on results from previous operations
- Complex business logic requires atomic execution
- Implementing financial transfers, inventory management, or state changes
Do NOT use for:
- Single operations (no transaction needed)
- Read-only operations (use batch queries instead)
- Independent operations that can fail separately
## $transaction Callback Pattern
```typescript
await prisma.$transaction(async (tx) => {
const result1 = await tx.model1.create({ data: { ... } });
const result2 = await tx.model2.update({
where: { id: result1.relatedId },
data: { ... }
});
return { result1, result2 };
});
```
All operations use the `tx` client. If any operation throws, the entire transaction rolls back automatically.
## Banking Transfer Example
```typescript
async function transferMoney(fromId: string, toId: string, amount: number) {
return await prisma.$transaction(async (tx) => {
const fromAccount = await tx.account.findUnique({
where: { id: fromId }
});
if (!fromAccount || fromAccount.balance < amount) {
throw new Error('Insufficient funds');
}
const updatedFrom = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } }
});
const updatedTo = await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } }
});
const transfer = await tx.transfer.create({
data: {
fromAccountId: fromId,
toAccountId: toId,
amount
}
});
return { updatedFrom, updatedTo, transfer };
});
}
```
If any step fails, all changes roll back. Both accounts and the transfer record are consistent.
## Inventory Reservation Pattern
```typescript
async function reserveInventory(orderId: string, items: Array<{ productId: string; quantity: number }>) {
return await prisma.$transaction(async (tx) => {
const order = await tx.order.update({
where: { id: orderId },
data: { status: 'PROCESSING' }
});
for (const item of items) {
const product = await tx.product.findUnique({
where: { id: item.productId }
});
if (!product || product.stock < item.quantity) {
throw new Error(`Insufficient stock for product ${item.productId}`);
}
await tx.product.update({
where: { id: item.productId },
data: { stock: { decrement: item.quantity } }
});
await tx.orderItem.create({
data: {
orderId,
productId: item.productId,
quantity: item.quantity,
price: product.price
}
});
}
return await tx.order.update({
where: { id: orderId },
data: { status: 'RESERVED' }
});
});
}
```
If stock is insufficient for any item, the entire reservation rolls back. No partial inventory deductions occur.
## Multi-Step Atomic Operations
```typescript
async function createUserWithProfile(userData: UserData, profileData: ProfileData) {
return await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
email: userData.email,
name: userData.name
}
});
const profile = await tx.profile.create({
data: {
userId: user.id,
bio: profileData.bio,
avatar: profileData.avatar
}
});
await tx.notification.create({
data: {
userId: user.id,
message: 'Welcome to our platform!'
}
});
return { user, profile };
});
}
```
User, profile, and notification are created atomically. If profile creation fails, the user is not created.
## Error Handling and Rollback
```typescript
try {
const result = await prisma.$transaction(async (tx) => {
const step1 = await tx.model.create({ data: { ... } });
if (someCondition) {
throw new Error('Business rule violation');
}
const step2 = await tx.model.update({ ... });
return { step1, step2 };
});
} catch (error) {
console.error('Transaction failed, all changes rolled back:', error);
}
```
Any thrown error triggers automatic rollback. No manual cleanup needed.
## Transaction Timeout
```typescript
await prisma.$transaction(async (tx) => {
}, {
timeout: 10000
});
```
Default timeout is 5 seconds. Increase for long-running transactions.
## Isolation Level
```typescript
await prisma.$transaction(async (tx) => {
}, {
isolationLevel: 'Serializable'
});
```
Available levels: `ReadUncommitted`, `ReadCommitted`, `RepeatableRead`, `Serializable`. Default is database-specific.
## Common Patterns
**Conditional Rollback:**
```typescript
await prisma.$transaction(async (tx) => {
const record = await tx.model.create({ data: { ... } });
if (!isValid(record)) {
throw new Error('Validation failed');
}
return record;
});
```
**Dependent Operations:**
```typescript
await prisma.$transaction(async (tx) => {
const parent = await tx.parent.create({ data: { ... } });
const child = await tx.child.create({
data: { parentId: parent.id, ... }
});
return { parent, child };
});
```
**Batch with Validation:**
```typescript
await prisma.$transaction(async (tx) => {
const records = await Promise.all(
items.map(item => tx.model.create({ data: item }))
);
if (records.length !== items.length) {
throw new Error('Not all records created');
}
return records;
});
```
## Anti-Patterns
**Mixing transaction and non-transaction calls:**
```typescript
await prisma.$transaction(async (tx) => {
await tx.model.create({ data: { ... } });
await prisma.model.create({ data: { ... } });
});
```
Use `tx` for ALL operations inside the transaction.
**Long-running operations:**
```typescript
await prisma.$transaction(async (tx) => {
await tx.model.create({ data: { ... } });
await fetch('https://api.example.com');
await tx.model.update({ ... });
});
```
Keep external API calls outside transactions. Transactions hold database locks.
**Catching errors inside transaction:**
```typescript
await prisma.$transaction(async (tx) => {
try {
await tx.model.create({ data: { ... } });
} catch (error) {
}
await tx.model.update({ ... });
});
```
Let errors propagate to trigger rollback. Handle errors outside the transaction.
## Implementation Steps
1. **Identify atomic operations** that must succeed or fail together
2. **Wrap in $transaction callback** with `async (tx) => { ... }`
3. **Use tx client** for all operations inside the callback
4. **Validate before operations** to fail fast
5. **Throw errors** to trigger rollback on business rule violations
6. **Return result** from the callback for success case
7. **Handle errors** outside the transaction for logging/recovery
## Verification
After implementing interactive transactions:
1. Test successful execution path
2. Test rollback on database errors
3. Test rollback on business rule violations
4. Verify no partial state changes after failures
5. Check transaction timeout for long operations
6. Validate isolation level for concurrent access
Interactive transactions ensure data consistency through automatic rollback and atomic execution.

View File

@@ -0,0 +1,433 @@
---
name: validating-query-inputs
description: Validate all external input with Zod before Prisma operations. Use when accepting user input, API requests, or form data.
allowed-tools: Read, Write, Edit
---
# Input Validation with Zod and Prisma 6
## Overview
Always validate external input with Zod before Prisma operations. Never trust user-provided data, API requests, or form submissions. Use type-safe validation pipelines that match Prisma schema types.
## Validation Pipeline
```
External Input → Zod Validation → Transform → Prisma Operation
```
### Pattern
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().positive().optional()
})
async function createUser(rawInput: unknown) {
const validatedData = createUserSchema.parse(rawInput)
return await prisma.user.create({
data: validatedData
})
}
```
## Zod Schemas for Prisma Models
### Matching Prisma Types
```prisma
model User {
id String @id @default(cuid())
email String @unique
name String
phone String?
website String?
age Int?
createdAt DateTime @default(now())
}
```
```typescript
import { z } from 'zod'
const phoneRegex = /^\+?[1-9]\d{1,14}$/
const userCreateSchema = z.object({
email: z.string().email().toLowerCase(),
name: z.string().min(1).max(100).trim(),
phone: z.string().regex(phoneRegex).optional(),
website: z.string().url().optional(),
age: z.number().int().min(0).max(150).optional()
})
const userUpdateSchema = userCreateSchema.partial()
type UserCreateInput = z.infer<typeof userCreateSchema>
type UserUpdateInput = z.infer<typeof userUpdateSchema>
```
### Common Validation Patterns
```typescript
const emailSchema = z.string().email().toLowerCase().trim()
const urlSchema = z.string().url().refine(
(url) => url.startsWith('https://'),
{ message: 'URL must use HTTPS' }
)
const phoneSchema = z.string().regex(
/^\+?[1-9]\d{1,14}$/,
'Invalid phone number format'
)
const slugSchema = z.string()
.min(1)
.max(100)
.regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens')
const dateSchema = z.coerce.date().refine(
(date) => date > new Date(),
{ message: 'Date must be in the future' }
)
```
## Complete Validation Examples
### API Route with Validation
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
authorId: z.string().cuid(),
published: z.boolean().default(false),
tags: z.array(z.string()).max(10).optional()
})
export async function POST(request: Request) {
try {
const rawBody = await request.json()
const validatedData = createPostSchema.parse(rawBody)
const post = await prisma.post.create({
data: validatedData
})
return Response.json(post)
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{ errors: error.errors },
{ status: 400 }
)
}
throw error
}
}
```
### Form Data Validation
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const profileUpdateSchema = z.object({
name: z.string().min(1).max(100).trim(),
bio: z.string().max(500).trim().optional(),
website: z.string().url().optional().or(z.literal('')),
location: z.string().max(100).trim().optional(),
birthDate: z.coerce.date().max(new Date()).optional()
})
async function updateProfile(userId: string, formData: FormData) {
const rawData = {
name: formData.get('name'),
bio: formData.get('bio'),
website: formData.get('website'),
location: formData.get('location'),
birthDate: formData.get('birthDate')
}
const validatedData = profileUpdateSchema.parse(rawData)
return await prisma.user.update({
where: { id: userId },
data: validatedData
})
}
```
### Nested Object Validation
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const addressSchema = z.object({
street: z.string().min(1).max(200),
city: z.string().min(1).max(100),
state: z.string().length(2).toUpperCase(),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
})
const createCompanySchema = z.object({
name: z.string().min(1).max(200),
email: z.string().email().toLowerCase(),
website: z.string().url().optional(),
address: addressSchema
})
async function createCompany(rawInput: unknown) {
const validatedData = createCompanySchema.parse(rawInput)
return await prisma.company.create({
data: {
name: validatedData.name,
email: validatedData.email,
website: validatedData.website,
address: {
create: validatedData.address
}
},
include: {
address: true
}
})
}
```
### Bulk Operation Validation
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const bulkUserSchema = z.object({
users: z.array(
z.object({
email: z.string().email().toLowerCase(),
name: z.string().min(1).max(100),
role: z.enum(['USER', 'ADMIN'])
})
).min(1).max(100)
})
async function createBulkUsers(rawInput: unknown) {
const validatedData = bulkUserSchema.parse(rawInput)
const uniqueEmails = new Set(validatedData.users.map(u => u.email))
if (uniqueEmails.size !== validatedData.users.length) {
throw new Error('Duplicate emails in bulk operation')
}
return await prisma.$transaction(
validatedData.users.map(user =>
prisma.user.create({ data: user })
)
)
}
```
## Advanced Patterns
### Custom Refinements
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const passwordSchema = z.string()
.min(8)
.refine((pwd) => /[A-Z]/.test(pwd), {
message: 'Password must contain uppercase letter'
})
.refine((pwd) => /[a-z]/.test(pwd), {
message: 'Password must contain lowercase letter'
})
.refine((pwd) => /[0-9]/.test(pwd), {
message: 'Password must contain number'
})
const registerSchema = z.object({
email: z.string().email().toLowerCase(),
password: passwordSchema,
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword']
})
```
### Async Validation
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const createUserSchema = z.object({
email: z.string().email().toLowerCase(),
username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/)
})
async function createUserWithChecks(rawInput: unknown) {
const validatedData = createUserSchema.parse(rawInput)
const existing = await prisma.user.findFirst({
where: {
OR: [
{ email: validatedData.email },
{ username: validatedData.username }
]
}
})
if (existing) {
if (existing.email === validatedData.email) {
throw new Error('Email already exists')
}
if (existing.username === validatedData.username) {
throw new Error('Username already taken')
}
}
return await prisma.user.create({
data: validatedData
})
}
```
### Safe Parsing
```typescript
import { z } from 'zod'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const updateSettingsSchema = z.object({
theme: z.enum(['light', 'dark']).default('light'),
notifications: z.boolean().default(true),
language: z.string().length(2).default('en')
})
async function updateSettings(userId: string, rawInput: unknown) {
const result = updateSettingsSchema.safeParse(rawInput)
if (!result.success) {
return {
success: false,
errors: result.error.errors
}
}
const settings = await prisma.userSettings.upsert({
where: { userId },
update: result.data,
create: {
userId,
...result.data
}
})
return {
success: true,
data: settings
}
}
```
## Security Checklist
- [ ] All external input validated before Prisma operations
- [ ] Zod schemas match Prisma model types
- [ ] Email addresses normalized (toLowerCase)
- [ ] String inputs trimmed where appropriate
- [ ] URLs validated and HTTPS enforced
- [ ] Phone numbers validated with regex
- [ ] Numeric ranges validated (min/max)
- [ ] Array lengths limited (prevent DoS)
- [ ] Unique constraints validated before bulk operations
- [ ] Async existence checks for unique fields
- [ ] Error messages don't leak sensitive data
- [ ] File uploads validated (type, size, content)
## Anti-Patterns
### Never Trust Input
```typescript
async function createUser(data: any) {
return await prisma.user.create({ data })
}
```
### Never Skip Validation for "Internal" Data
```typescript
async function createUserFromAdmin(data: unknown) {
return await prisma.user.create({ data })
}
```
### Never Validate After Database Operation
```typescript
async function createUser(data: unknown) {
const user = await prisma.user.create({ data })
const validated = schema.parse(user)
return validated
}
```
## Type Safety Integration
```typescript
import { z } from 'zod'
import { Prisma } from '@prisma/client'
const userCreateSchema = z.object({
email: z.string().email(),
name: z.string()
}) satisfies z.Schema<Prisma.UserCreateInput>
type ValidatedUserInput = z.infer<typeof userCreateSchema>
```
## Related Skills
**Zod v4 Validation:**
- If normalizing string inputs (trim, toLowerCase), use the transforming-string-methods skill for Zod v4 built-in string transformations
- If using Zod for schema construction, use the validating-schema-basics skill from zod-4 for core validation patterns
- If customizing validation error messages, use the customizing-errors skill from zod-4 for error formatting strategies
- If validating string formats (email, UUID, URL), use the validating-string-formats skill from zod-4 for built-in validators
**TypeScript Validation:**
- If performing runtime type checking beyond Zod, use the using-runtime-checks skill from typescript for assertion patterns
- If validating external data sources, use the validating-external-data skill from typescript for comprehensive validation strategies