Files
pos-system/docs/en/skills/database-prisma.md
Ho Ngoc Hai 9b6c585f57 Enhance documentation structure and improve bilingual support across skills
- 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.
2026-01-01 07:35:44 +07:00

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

  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