--- name: database-prisma description: Prisma ORM and database patterns for GoodGo microservices. Use when working with databases, creating Prisma schemas, writing migrations, implementing repositories, or optimizing queries. --- # Prisma Database Patterns ## When to Use This Skill Use this skill when: - Setting up Prisma for a new service - Creating or modifying database schemas - Writing database migrations - Implementing repository patterns - Optimizing database queries - Setting up database connections - Implementing transactions - Working with Neon PostgreSQL ## Core Concepts ### Architecture - Repository pattern for data access - Prisma as ORM for type safety - Neon PostgreSQL as primary database - Connection pooling for performance - Transaction support for data consistency ## Prisma Setup ### Installation ```bash npm install @prisma/client prisma npm install --save-dev @types/node ``` ### Configuration ```typescript // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // Base model with common fields model User { id String @id @default(cuid()) email String @unique name String? password String role Role @default(USER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations posts Post[] profile Profile? // Indexes for performance @@index([email]) @@index([createdAt]) @@map("users") } model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) authorId String author User @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([authorId]) @@index([published, createdAt]) @@map("posts") } model Profile { id String @id @default(cuid()) bio String? avatar String? userId String @unique user User @relation(fields: [userId], references: [id]) @@map("profiles") } enum Role { USER ADMIN MODERATOR } ``` ### Schema Relationships The following diagram illustrates the relationships between User, Post, and Profile models: ```mermaid erDiagram User ||--o{ Post : "has many" User ||--o| Profile : "has one" User { string id PK string email UK string name string password enum role datetime createdAt datetime updatedAt } Post { string id PK string title string content boolean published string authorId FK datetime createdAt datetime updatedAt } Profile { string id PK string bio string avatar string userId FK,UK } ``` ## Database Connection ```typescript // src/lib/prisma.ts import { PrismaClient } from '@prisma/client'; const globalForPrisma = global 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; } // Middleware for soft delete prisma.$use(async (params, next) => { if (params.model && params.action === 'delete') { return next({ ...params, action: 'update', args: { ...params.args, data: { deletedAt: new Date() } } }); } return next(params); }); ``` ## Repository Pattern ```typescript // src/repositories/base.repository.ts export abstract class BaseRepository { constructor(protected prisma: PrismaClient) {} abstract findById(id: string): Promise; abstract findAll(options?: any): Promise; abstract create(data: any): Promise; abstract update(id: string, data: any): Promise; abstract delete(id: string): Promise; } // src/repositories/user.repository.ts export class UserRepository extends BaseRepository { async findById(id: string): Promise { return this.prisma.user.findUnique({ where: { id }, include: { profile: true } }); } async findByEmail(email: string): Promise { return this.prisma.user.findUnique({ where: { email } }); } async findAll(options: { page?: number; limit?: number; search?: string; sortBy?: string; order?: 'asc' | 'desc'; } = {}): Promise<{ data: User[]; total: number }> { const { page = 1, limit = 10, search, sortBy = 'createdAt', order = 'desc' } = options; const where = search ? { OR: [ { email: { contains: search, mode: 'insensitive' } }, { name: { contains: search, mode: 'insensitive' } } ] } : {}; const [data, total] = await Promise.all([ this.prisma.user.findMany({ where, skip: (page - 1) * limit, take: limit, orderBy: { [sortBy]: order }, include: { profile: true } }), this.prisma.user.count({ where }) ]); return { data, total }; } async create(data: CreateUserDto): Promise { return this.prisma.user.create({ data: { email: data.email, password: data.password, name: data.name, profile: data.bio ? { create: { bio: data.bio } } : undefined }, include: { profile: true } }); } async update(id: string, data: UpdateUserDto): Promise { return this.prisma.user.update({ where: { id }, data, include: { profile: true } }); } async delete(id: string): Promise { await this.prisma.user.delete({ where: { id } }); } } ``` ## Transactions ```typescript // Transaction example export class TransferService { async transferFunds( fromAccountId: string, toAccountId: string, amount: number ) { return await this.prisma.$transaction(async (tx) => { // Check balance const fromAccount = await tx.account.findUnique({ where: { id: fromAccountId } }); if (!fromAccount || fromAccount.balance < amount) { throw new Error('Insufficient funds'); } // Deduct from sender const updatedFrom = await tx.account.update({ where: { id: fromAccountId }, data: { balance: { decrement: amount } } }); // Add to receiver const updatedTo = await tx.account.update({ where: { id: toAccountId }, data: { balance: { increment: amount } } }); // Create transaction record const transaction = await tx.transaction.create({ data: { fromAccountId, toAccountId, amount, type: 'TRANSFER', status: 'COMPLETED' } }); return transaction; }, { maxWait: 5000, timeout: 10000, }); } } ``` ## Migrations ### Migration Workflow The following diagram shows the typical migration workflow from development to production: ```mermaid flowchart TD Start([Start Migration]) --> EditSchema[Edit schema.prisma] EditSchema --> CreateMigration[Run: prisma migrate dev] CreateMigration --> GenerateSQL[Prisma generates SQL] GenerateSQL --> ReviewSQL{Review SQL?} ReviewSQL -->|Yes| CheckSQL[Check migration SQL] ReviewSQL -->|No| ApplyDev[Apply to dev database] CheckSQL --> ApplyDev ApplyDev --> GenerateClient[Generate Prisma Client] GenerateClient --> TestDev[Test in development] TestDev --> TestPass{Tests pass?} TestPass -->|No| FixIssues[Fix issues] FixIssues --> EditSchema TestPass -->|Yes| CommitMigration[Commit migration files] CommitMigration --> DeployProd[Deploy to production] DeployProd --> RunDeploy[Run: prisma migrate deploy] RunDeploy --> End([Migration Complete]) style Start fill:#e1f5e1 style End fill:#e1f5e1 style TestPass fill:#fff4e1 style ReviewSQL fill:#fff4e1 ``` ```bash # Create migration npx prisma migrate dev --name add_user_table # Apply migrations npx prisma migrate deploy # Reset database npx prisma migrate reset # Generate Prisma Client npx prisma generate ``` ### Migration Files ```sql -- migrations/20240101000000_add_user_table/migration.sql CREATE TABLE "users" ( "id" TEXT NOT NULL, "email" TEXT NOT NULL, "name" TEXT, "password" TEXT NOT NULL, "role" TEXT NOT NULL DEFAULT 'USER', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "users_pkey" PRIMARY KEY ("id") ); CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); CREATE INDEX "users_createdAt_idx" ON "users"("createdAt"); ``` ## Query Optimization ### Query Execution Flow The following sequence diagram illustrates how Prisma queries flow from the application layer to the database: ```mermaid sequenceDiagram participant App as Application participant Repo as Repository participant Client as Prisma Client participant Pool as Connection Pool participant DB as PostgreSQL App->>Repo: findById(id) Repo->>Client: prisma.user.findUnique() Client->>Client: Validate query Client->>Client: Generate SQL Client->>Pool: Request connection Pool->>DB: Execute SQL query DB-->>Pool: Return results Pool-->>Client: Return data Client->>Client: Transform to TypeScript types Client-->>Repo: Return typed result Repo-->>App: Return User | null Note over App,DB: Prisma ensures type safety
throughout the flow ``` ```typescript // Optimized queries export class OptimizedUserRepository { // Select only needed fields async findUsersLight() { return this.prisma.user.findMany({ select: { id: true, email: true, name: true } }); } // Use pagination async findPaginated(cursor?: string) { return this.prisma.user.findMany({ take: 10, skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, orderBy: { createdAt: 'desc' } }); } // Batch operations async createMany(users: CreateUserDto[]) { return this.prisma.user.createMany({ data: users, skipDuplicates: true }); } // Use raw SQL for complex queries async getStatistics() { return this.prisma.$queryRaw` SELECT COUNT(*) as total, COUNT(CASE WHEN role = 'ADMIN' THEN 1 END) as admins, COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as new_users FROM users `; } } ``` ## Seeding ```typescript // prisma/seed.ts import { PrismaClient } from '@prisma/client'; import bcrypt from 'bcrypt'; const prisma = new PrismaClient(); async function main() { // Create admin user const adminPassword = await bcrypt.hash('admin123', 10); const admin = await prisma.user.upsert({ where: { email: 'admin@goodgo.com' }, update: {}, create: { email: 'admin@goodgo.com', name: 'Admin User', password: adminPassword, role: 'ADMIN' } }); // Create test users const testUsers = Array.from({ length: 10 }, (_, i) => ({ email: `user${i}@example.com`, name: `Test User ${i}`, password: bcrypt.hashSync('password123', 10) })); await prisma.user.createMany({ data: testUsers, skipDuplicates: true }); console.log('Database seeded successfully'); } main() .catch(console.error) .finally(() => prisma.$disconnect()); ``` ## Neon PostgreSQL Configuration ```typescript // .env DATABASE_URL="postgresql://user:password@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require" // Connection pooling for serverless DIRECT_URL="postgresql://user:password@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require" ``` ## Testing with Prisma ```typescript // __tests__/user.repository.test.ts import { mockDeep, mockReset } from 'jest-mock-extended'; import { PrismaClient } from '@prisma/client'; jest.mock('../src/lib/prisma', () => ({ __esModule: true, prisma: mockDeep() })); describe('UserRepository', () => { beforeEach(() => { mockReset(prismaMock); }); it('should create user', async () => { const user = { id: '1', email: 'test@example.com' }; prismaMock.user.create.mockResolvedValue(user); const result = await repository.create({ email: 'test@example.com', password: 'password' }); expect(result).toEqual(user); }); }); ``` ## Best Practices 1. **Schema Design** - Use appropriate field types - Add indexes for frequently queried fields - Use relations instead of storing JSON - Implement soft deletes when needed 2. **Performance** - Use select to fetch only needed fields - Implement pagination for large datasets - Use connection pooling - Cache frequently accessed data 3. **Security** - Never expose sensitive fields - Use parameterized queries - Validate input before database operations - Implement row-level security 4. **Maintenance** - Keep migrations small and focused - Test migrations before production - Backup before major changes - Monitor query performance