- Updated skill documentation files to include structured metadata for better organization. - Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese. - Refined sections on usage, best practices, and related skills to ensure consistency across all documentation. - Improved formatting and removed outdated references to streamline the documentation experience. - Added best practices checklists to relevant skills for better usability and adherence to standards.
478 lines
10 KiB
Markdown
478 lines
10 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
|
|
}
|
|
```
|
|
|
|
## 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
|
|
|
|
```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
|
|
|
|
```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 |