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

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

  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