- 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.
10 KiB
10 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
}
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
# 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
// 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