- 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.
13 KiB
13 KiB
name, description
| name | description |
|---|---|
| database-prisma | 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
npm install @prisma/client prisma
npm install --save-dev @types/node
Configuration
// 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:
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
// 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
// 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
// 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:
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
# 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
-- 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:
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
// 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
// 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
// .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
// __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
-
Schema Design
- Use appropriate field types
- Add indexes for frequently queried fields
- Use relations instead of storing JSON
- Implement soft deletes when needed
-
Performance
- Use select to fetch only needed fields
- Implement pagination for large datasets
- Use connection pooling
- Cache frequently accessed data
-
Security
- Never expose sensitive fields
- Use parameterized queries
- Validate input before database operations
- Implement row-level security
-
Maintenance
- Keep migrations small and focused
- Test migrations before production
- Backup before major changes
- Monitor query performance