- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes. - Introduced configuration loading flow in configuration-management skill to clarify the configuration process. - Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers. - Enhanced various skills with additional diagrams to improve understanding of complex concepts. These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
571 lines
13 KiB
Markdown
571 lines
13 KiB
Markdown
---
|
|
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<T> {
|
|
constructor(protected prisma: PrismaClient) {}
|
|
|
|
abstract findById(id: string): Promise<T | null>;
|
|
abstract findAll(options?: any): Promise<T[]>;
|
|
abstract create(data: any): Promise<T>;
|
|
abstract update(id: string, data: any): Promise<T>;
|
|
abstract delete(id: string): Promise<void>;
|
|
}
|
|
|
|
// src/repositories/user.repository.ts
|
|
export class UserRepository extends BaseRepository<User> {
|
|
async findById(id: string): Promise<User | null> {
|
|
return this.prisma.user.findUnique({
|
|
where: { id },
|
|
include: { profile: true }
|
|
});
|
|
}
|
|
|
|
async findByEmail(email: string): Promise<User | null> {
|
|
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<User> {
|
|
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<User> {
|
|
return this.prisma.user.update({
|
|
where: { id },
|
|
data,
|
|
include: { profile: true }
|
|
});
|
|
}
|
|
|
|
async delete(id: string): Promise<void> {
|
|
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<br/>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<PrismaClient>()
|
|
}));
|
|
|
|
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 |