Initial commit
This commit is contained in:
159
skills/configuring-connection-pools/SKILL.md
Normal file
159
skills/configuring-connection-pools/SKILL.md
Normal 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 | 10–20 | Total connections = instances × limit |
|
||||
| `pool_timeout` | 20–30 sec | 0 (fail fast) | — | Wait time for available connection |
|
||||
| `connect_timeout` | 5 sec | 10 sec | — | Initial connection timeout; 15–30 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.
|
||||
163
skills/configuring-serverless-clients/SKILL.md
Normal file
163
skills/configuring-serverless-clients/SKILL.md
Normal 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`
|
||||
347
skills/configuring-transaction-isolation/SKILL.md
Normal file
347
skills/configuring-transaction-isolation/SKILL.md
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -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.
|
||||
173
skills/creating-client-singletons/SKILL.md
Normal file
173
skills/creating-client-singletons/SKILL.md
Normal 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
|
||||
310
skills/creating-client-singletons/references/common-scenarios.md
Normal file
310
skills/creating-client-singletons/references/common-scenarios.md
Normal 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.
|
||||
@@ -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
|
||||
259
skills/creating-client-singletons/references/test-pattern.md
Normal file
259
skills/creating-client-singletons/references/test-pattern.md
Normal 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
|
||||
167
skills/deploying-production-migrations/SKILL.md
Normal file
167
skills/deploying-production-migrations/SKILL.md
Normal 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)
|
||||
288
skills/ensuring-query-type-safety/SKILL.md
Normal file
288
skills/ensuring-query-type-safety/SKILL.md
Normal 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
|
||||
509
skills/handling-transaction-errors/SKILL.md
Normal file
509
skills/handling-transaction-errors/SKILL.md
Normal 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
|
||||
239
skills/implementing-query-caching/SKILL.md
Normal file
239
skills/implementing-query-caching/SKILL.md
Normal 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
|
||||
@@ -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.
|
||||
160
skills/implementing-query-caching/references/common-pitfalls.md
Normal file
160
skills/implementing-query-caching/references/common-pitfalls.md
Normal 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
|
||||
}
|
||||
```
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
213
skills/implementing-query-pagination/SKILL.md
Normal file
213
skills/implementing-query-pagination/SKILL.md
Normal 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), 10k–100k (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 (5–50ms); 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
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
@@ -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])
|
||||
}
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
|
||||
184
skills/managing-client-lifecycle/SKILL.md
Normal file
184
skills/managing-client-lifecycle/SKILL.md
Normal 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
|
||||
115
skills/managing-dev-migrations/SKILL.md
Normal file
115
skills/managing-dev-migrations/SKILL.md
Normal 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.
|
||||
76
skills/optimizing-query-performance/SKILL.md
Normal file
76
skills/optimizing-query-performance/SKILL.md
Normal 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
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
271
skills/optimizing-query-selection/SKILL.md
Normal file
271
skills/optimizing-query-selection/SKILL.md
Normal 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
|
||||
139
skills/optimizing-query-selection/references/api-optimization.md
Normal file
139
skills/optimizing-query-selection/references/api-optimization.md
Normal 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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
101
skills/optimizing-query-selection/references/type-safety.md
Normal file
101
skills/optimizing-query-selection/references/type-safety.md
Normal 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
|
||||
483
skills/preventing-error-exposure/SKILL.md
Normal file
483
skills/preventing-error-exposure/SKILL.md
Normal 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
|
||||
441
skills/preventing-sql-injection/SKILL.md
Normal file
441
skills/preventing-sql-injection/SKILL.md
Normal 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
|
||||
256
skills/reviewing-prisma-patterns/SKILL.md
Normal file
256
skills/reviewing-prisma-patterns/SKILL.md
Normal 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.
|
||||
446
skills/reviewing-prisma-patterns/references/example-reviews.md
Normal file
446
skills/reviewing-prisma-patterns/references/example-reviews.md
Normal 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.
|
||||
433
skills/reviewing-prisma-patterns/references/validation-checks.md
Normal file
433
skills/reviewing-prisma-patterns/references/validation-checks.md
Normal 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 |
|
||||
194
skills/upgrading-to-prisma-6/SKILL.md
Normal file
194
skills/upgrading-to-prisma-6/SKILL.md
Normal 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
|
||||
173
skills/upgrading-to-prisma-6/references/breaking-changes.md
Normal file
173
skills/upgrading-to-prisma-6/references/breaking-changes.md
Normal 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`
|
||||
@@ -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
|
||||
193
skills/upgrading-to-prisma-6/references/migration-examples.md
Normal file
193
skills/upgrading-to-prisma-6/references/migration-examples.md
Normal 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.
|
||||
45
skills/upgrading-to-prisma-6/references/troubleshooting.md
Normal file
45
skills/upgrading-to-prisma-6/references/troubleshooting.md
Normal 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.
|
||||
305
skills/using-interactive-transactions/SKILL.md
Normal file
305
skills/using-interactive-transactions/SKILL.md
Normal 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.
|
||||
433
skills/validating-query-inputs/SKILL.md
Normal file
433
skills/validating-query-inputs/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user