Files
pos-system/docs/en/skills/database-prisma.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- 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.
2026-01-01 23:22:54 +07:00

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