
prisma-patterns
by mattleonard16
SKILL.md
name: prisma-patterns description: Prisma schema conventions, migrations, and query patterns for TaxHelper. Use when modifying the database schema, writing complex queries, or optimizing database access.
Prisma Patterns Skill
This skill provides guidance for working with Prisma in TaxHelper.
When to Use This Skill
- Modifying the database schema
- Creating migrations
- Writing efficient queries
- Adding indexes for performance
- Working with Decimal types
- Implementing the repository pattern
Project Setup
- Schema:
prisma/schema.prisma - Client:
src/lib/prisma.ts - Migrations:
prisma/migrations/
Commands
# Validate schema
npx prisma validate
# Create migration
npx prisma migrate dev --name descriptive_name
# Apply migrations (production)
npx prisma migrate deploy
# Generate client after schema changes
npx prisma generate
# Reset database (development only)
npx prisma migrate reset
# Open Prisma Studio
npx prisma studio
Schema Conventions
Model Structure
model Transaction {
// 1. Primary key
id String @id @default(cuid())
// 2. Foreign keys and relations
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// 3. Core fields
date DateTime
type TransactionType
description String?
merchant String?
// 4. Money fields (always Decimal)
totalAmount Decimal @db.Decimal(12, 2)
taxAmount Decimal @db.Decimal(12, 2)
// 5. Metadata
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// 6. Indexes at bottom
@@index([userId])
@@index([date])
@@index([userId, date])
}
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Models | PascalCase singular | Transaction, User |
| Fields | camelCase | userId, totalAmount |
| Enums | PascalCase | TransactionType |
| Enum values | SCREAMING_SNAKE | SALES_TAX, INCOME_TAX |
| Indexes | Implicit naming | @@index([userId, date]) |
Money Fields
Always use Decimal for monetary values:
totalAmount Decimal @db.Decimal(12, 2) // Up to 9,999,999,999.99
taxRate Decimal @db.Decimal(10, 6) // 0.088750 for 8.875%
Enums
Define enums at the bottom of schema:
enum TransactionType {
SALES_TAX
INCOME_TAX
OTHER
}
enum ReceiptJobStatus {
QUEUED
PROCESSING
NEEDS_REVIEW
COMPLETED
CONFIRMED
FAILED
}
Indexing Strategy
Single-Column Indexes
Add for frequently filtered/sorted columns:
@@index([userId]) // Filter by user
@@index([date]) // Sort by date
@@index([type]) // Filter by type
@@index([status]) // Filter by status
Composite Indexes
Add for common query patterns (order matters!):
@@index([userId, date]) // User's transactions by date
@@index([userId, type, date]) // User's transactions filtered by type
@@index([userId, status]) // User's jobs by status
@@index([userId, status, createdAt]) // Inbox list query
Unique Constraints
@@unique([userId, date]) // One record per user per day
@@unique([provider, providerAccountId]) // OAuth uniqueness
transactionId String? @unique // Prevent duplicate links
Query Patterns
Basic CRUD
import { prisma } from "@/lib/prisma";
// Create
const transaction = await prisma.transaction.create({
data: {
userId: user.id,
date: new Date(),
type: "SALES_TAX",
totalAmount: new Prisma.Decimal("100.00"),
taxAmount: new Prisma.Decimal("8.88"),
},
});
// Read with filter
const transactions = await prisma.transaction.findMany({
where: {
userId: user.id,
date: { gte: startDate, lte: endDate },
},
orderBy: { date: "desc" },
take: 20,
skip: 0,
});
// Update
await prisma.transaction.update({
where: { id, userId: user.id },
data: { merchant: "Updated Merchant" },
});
// Delete
await prisma.transaction.delete({
where: { id, userId: user.id },
});
Aggregations
// Sum and count
const stats = await prisma.transaction.aggregate({
where: { userId: user.id },
_sum: { taxAmount: true, totalAmount: true },
_count: true,
_avg: { taxAmount: true },
});
// Access results
const totalTax = stats._sum.taxAmount || new Prisma.Decimal(0);
const count = stats._count;
Group By
const byType = await prisma.transaction.groupBy({
by: ["type"],
where: { userId: user.id },
_sum: { taxAmount: true, totalAmount: true },
});
// Transform to object
const result: Record<string, Prisma.Decimal> = {};
for (const item of byType) {
result[item.type] = item._sum.taxAmount || new Prisma.Decimal(0);
}
Top N with Aggregation
const topMerchants = await prisma.transaction.groupBy({
by: ["merchant"],
where: {
userId: user.id,
merchant: { not: null },
},
_sum: { taxAmount: true },
orderBy: { _sum: { taxAmount: "desc" } },
take: 5,
});
Raw SQL for Complex Queries
const dailyTotals = await prisma.$queryRaw<
Array<{ date_key: Date; total_tax: Prisma.Decimal }>
>`
SELECT
DATE("date") as date_key,
SUM("taxAmount") as total_tax
FROM "Transaction"
WHERE "userId" = ${user.id}
AND "date" >= ${fromDate}
AND "date" <= ${toDate}
GROUP BY DATE("date")
ORDER BY date_key ASC
`;
Transactions (Atomic Operations)
const [job, transaction] = await prisma.$transaction([
prisma.receiptJob.update({
where: { id: jobId },
data: { status: "CONFIRMED", transactionId },
}),
prisma.transaction.create({
data: { ... },
}),
]);
// Or with callback for dependent operations
await prisma.$transaction(async (tx) => {
const job = await tx.receiptJob.findUnique({ where: { id: jobId } });
if (!job) throw new Error("Job not found");
const transaction = await tx.transaction.create({ data: { ... } });
await tx.receiptJob.update({
where: { id: jobId },
data: { transactionId: transaction.id },
});
});
Decimal Handling
In API Responses
Always convert to string for JSON:
const response = {
totalAmount: transaction.totalAmount.toString(),
taxAmount: transaction.taxAmount.toString(),
};
In Calculations
import { Prisma } from "@prisma/client";
// Create from string
const amount = new Prisma.Decimal("100.50");
// Arithmetic
const result = amount.add(other);
const result = amount.sub(other);
const result = amount.mul(rate);
const result = amount.div(divisor);
// Comparison
if (amount.isZero()) { ... }
if (amount.gt(other)) { ... }
if (amount.lte(other)) { ... }
// Formatting
const rounded = amount.toDecimalPlaces(2);
const number = amount.toNumber(); // Use sparingly, loses precision
Repository Pattern
Create repository files for complex data access:
// src/lib/receipt/receipt-job-repository.ts
import { prisma } from "@/lib/prisma";
import { ReceiptJobStatus } from "@prisma/client";
export async function findPendingJobs(userId: string, limit = 10) {
return prisma.receiptJob.findMany({
where: { userId, status: "QUEUED" },
orderBy: { createdAt: "asc" },
take: limit,
});
}
export async function updateJobStatus(
id: string,
status: ReceiptJobStatus,
data?: Partial<{ lastError: string; processedAt: Date }>
) {
return prisma.receiptJob.update({
where: { id },
data: { status, ...data, updatedAt: new Date() },
});
}
Migration Best Practices
-
Descriptive names:
npx prisma migrate dev --name add_category_to_transactions -
Non-breaking changes first: Add nullable columns before making them required
-
Data migrations: Use separate scripts, not Prisma migrations
-
Review generated SQL: Check
prisma/migrations/*/migration.sql -
Test migrations: Reset dev database and run all migrations
npx prisma migrate reset # Development only!
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です