omgkit 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,55 +1,801 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prisma
|
|
3
|
-
description: Prisma ORM
|
|
3
|
+
description: Prisma ORM with type-safe queries, migrations, relations, and production database patterns
|
|
4
|
+
category: databases
|
|
5
|
+
triggers:
|
|
6
|
+
- prisma
|
|
7
|
+
- prisma client
|
|
8
|
+
- prisma migrate
|
|
9
|
+
- prisma schema
|
|
10
|
+
- orm
|
|
11
|
+
- type-safe database
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
# Prisma
|
|
14
|
+
# Prisma
|
|
15
|
+
|
|
16
|
+
Enterprise-grade **Prisma ORM** development following industry best practices. This skill covers schema design, type-safe queries, migrations, relations, transactions, and production-ready patterns used by top engineering teams.
|
|
17
|
+
|
|
18
|
+
## Purpose
|
|
19
|
+
|
|
20
|
+
Build type-safe database applications with confidence:
|
|
21
|
+
|
|
22
|
+
- Design comprehensive Prisma schemas
|
|
23
|
+
- Write type-safe database queries
|
|
24
|
+
- Manage database migrations
|
|
25
|
+
- Handle complex relations
|
|
26
|
+
- Implement transactions
|
|
27
|
+
- Optimize query performance
|
|
28
|
+
- Deploy to production
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
### 1. Schema Design
|
|
7
33
|
|
|
8
|
-
## Schema
|
|
9
34
|
```prisma
|
|
35
|
+
// prisma/schema.prisma
|
|
36
|
+
generator client {
|
|
37
|
+
provider = "prisma-client-js"
|
|
38
|
+
previewFeatures = ["fullTextSearch", "fullTextIndex"]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
datasource db {
|
|
42
|
+
provider = "postgresql"
|
|
43
|
+
url = env("DATABASE_URL")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Enums
|
|
47
|
+
enum UserRole {
|
|
48
|
+
ADMIN
|
|
49
|
+
USER
|
|
50
|
+
GUEST
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
enum OrderStatus {
|
|
54
|
+
PENDING
|
|
55
|
+
CONFIRMED
|
|
56
|
+
SHIPPED
|
|
57
|
+
DELIVERED
|
|
58
|
+
CANCELLED
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// User model
|
|
10
62
|
model User {
|
|
11
63
|
id String @id @default(cuid())
|
|
12
64
|
email String @unique
|
|
13
|
-
|
|
65
|
+
password String
|
|
66
|
+
role UserRole @default(USER)
|
|
67
|
+
isActive Boolean @default(true)
|
|
14
68
|
createdAt DateTime @default(now())
|
|
69
|
+
updatedAt DateTime @updatedAt
|
|
70
|
+
|
|
71
|
+
// Relations
|
|
72
|
+
profile Profile?
|
|
73
|
+
posts Post[]
|
|
74
|
+
orders Order[]
|
|
75
|
+
memberships Membership[]
|
|
76
|
+
ownedOrgs Organization[] @relation("OrganizationOwner")
|
|
77
|
+
|
|
78
|
+
@@index([email])
|
|
79
|
+
@@index([role, isActive])
|
|
80
|
+
@@map("users")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// One-to-one relation
|
|
84
|
+
model Profile {
|
|
85
|
+
id String @id @default(cuid())
|
|
86
|
+
firstName String
|
|
87
|
+
lastName String
|
|
88
|
+
avatar String?
|
|
89
|
+
bio String?
|
|
90
|
+
|
|
91
|
+
userId String @unique
|
|
92
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
93
|
+
|
|
94
|
+
@@map("profiles")
|
|
15
95
|
}
|
|
16
96
|
|
|
97
|
+
// One-to-many relation
|
|
17
98
|
model Post {
|
|
18
|
-
id
|
|
19
|
-
title
|
|
20
|
-
|
|
99
|
+
id String @id @default(cuid())
|
|
100
|
+
title String
|
|
101
|
+
slug String @unique
|
|
102
|
+
content String
|
|
103
|
+
published Boolean @default(false)
|
|
104
|
+
publishedAt DateTime?
|
|
105
|
+
createdAt DateTime @default(now())
|
|
106
|
+
updatedAt DateTime @updatedAt
|
|
107
|
+
|
|
21
108
|
authorId String
|
|
109
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
110
|
+
|
|
111
|
+
categories Category[]
|
|
112
|
+
tags Tag[]
|
|
113
|
+
|
|
114
|
+
@@index([authorId])
|
|
115
|
+
@@index([published, publishedAt])
|
|
116
|
+
@@fulltext([title, content])
|
|
117
|
+
@@map("posts")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Many-to-many relation (explicit)
|
|
121
|
+
model Category {
|
|
122
|
+
id String @id @default(cuid())
|
|
123
|
+
name String @unique
|
|
124
|
+
slug String @unique
|
|
125
|
+
posts Post[]
|
|
126
|
+
|
|
127
|
+
@@map("categories")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
model Tag {
|
|
131
|
+
id String @id @default(cuid())
|
|
132
|
+
name String @unique
|
|
133
|
+
posts Post[]
|
|
134
|
+
|
|
135
|
+
@@map("tags")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Order with items (one-to-many)
|
|
139
|
+
model Order {
|
|
140
|
+
id String @id @default(cuid())
|
|
141
|
+
status OrderStatus @default(PENDING)
|
|
142
|
+
total Decimal @db.Decimal(10, 2)
|
|
143
|
+
createdAt DateTime @default(now())
|
|
144
|
+
updatedAt DateTime @updatedAt
|
|
145
|
+
|
|
146
|
+
userId String
|
|
147
|
+
user User @relation(fields: [userId], references: [id])
|
|
148
|
+
|
|
149
|
+
items OrderItem[]
|
|
150
|
+
|
|
151
|
+
@@index([userId, status])
|
|
152
|
+
@@index([createdAt])
|
|
153
|
+
@@map("orders")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
model OrderItem {
|
|
157
|
+
id String @id @default(cuid())
|
|
158
|
+
quantity Int
|
|
159
|
+
price Decimal @db.Decimal(10, 2)
|
|
160
|
+
|
|
161
|
+
orderId String
|
|
162
|
+
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
|
163
|
+
|
|
164
|
+
productId String
|
|
165
|
+
product Product @relation(fields: [productId], references: [id])
|
|
166
|
+
|
|
167
|
+
@@map("order_items")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
model Product {
|
|
171
|
+
id String @id @default(cuid())
|
|
172
|
+
name String
|
|
173
|
+
description String?
|
|
174
|
+
price Decimal @db.Decimal(10, 2)
|
|
175
|
+
stock Int @default(0)
|
|
176
|
+
createdAt DateTime @default(now())
|
|
177
|
+
|
|
178
|
+
orderItems OrderItem[]
|
|
179
|
+
|
|
180
|
+
@@map("products")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Many-to-many with extra fields
|
|
184
|
+
model Organization {
|
|
185
|
+
id String @id @default(cuid())
|
|
186
|
+
name String
|
|
187
|
+
slug String @unique
|
|
188
|
+
createdAt DateTime @default(now())
|
|
189
|
+
|
|
190
|
+
ownerId String
|
|
191
|
+
owner User @relation("OrganizationOwner", fields: [ownerId], references: [id])
|
|
192
|
+
|
|
193
|
+
memberships Membership[]
|
|
194
|
+
|
|
195
|
+
@@map("organizations")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
model Membership {
|
|
199
|
+
id String @id @default(cuid())
|
|
200
|
+
role String @default("member")
|
|
201
|
+
joinedAt DateTime @default(now())
|
|
202
|
+
|
|
203
|
+
userId String
|
|
204
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
205
|
+
|
|
206
|
+
organizationId String
|
|
207
|
+
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
|
208
|
+
|
|
209
|
+
@@unique([userId, organizationId])
|
|
210
|
+
@@map("memberships")
|
|
22
211
|
}
|
|
23
212
|
```
|
|
24
213
|
|
|
25
|
-
|
|
214
|
+
### 2. Type-Safe Queries
|
|
215
|
+
|
|
26
216
|
```typescript
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
data: { email: 'test@example.com' }
|
|
30
|
-
});
|
|
217
|
+
// src/repositories/user.repository.ts
|
|
218
|
+
import { PrismaClient, Prisma, User, UserRole } from '@prisma/client';
|
|
31
219
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
220
|
+
const prisma = new PrismaClient();
|
|
221
|
+
|
|
222
|
+
// Include types for relations
|
|
223
|
+
type UserWithProfile = Prisma.UserGetPayload<{
|
|
224
|
+
include: { profile: true };
|
|
225
|
+
}>;
|
|
226
|
+
|
|
227
|
+
type UserWithOrganizations = Prisma.UserGetPayload<{
|
|
228
|
+
include: {
|
|
229
|
+
memberships: {
|
|
230
|
+
include: { organization: true };
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
}>;
|
|
234
|
+
|
|
235
|
+
export class UserRepository {
|
|
236
|
+
// Create with profile
|
|
237
|
+
async createWithProfile(data: {
|
|
238
|
+
email: string;
|
|
239
|
+
password: string;
|
|
240
|
+
profile: { firstName: string; lastName: string };
|
|
241
|
+
}): Promise<UserWithProfile> {
|
|
242
|
+
return prisma.user.create({
|
|
243
|
+
data: {
|
|
244
|
+
email: data.email,
|
|
245
|
+
password: data.password,
|
|
246
|
+
profile: {
|
|
247
|
+
create: {
|
|
248
|
+
firstName: data.profile.firstName,
|
|
249
|
+
lastName: data.profile.lastName,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
include: { profile: true },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Find with filters
|
|
258
|
+
async findMany(params: {
|
|
259
|
+
skip?: number;
|
|
260
|
+
take?: number;
|
|
261
|
+
where?: Prisma.UserWhereInput;
|
|
262
|
+
orderBy?: Prisma.UserOrderByWithRelationInput;
|
|
263
|
+
}) {
|
|
264
|
+
const { skip, take, where, orderBy } = params;
|
|
265
|
+
|
|
266
|
+
const [users, total] = await prisma.$transaction([
|
|
267
|
+
prisma.user.findMany({
|
|
268
|
+
skip,
|
|
269
|
+
take,
|
|
270
|
+
where,
|
|
271
|
+
orderBy,
|
|
272
|
+
include: { profile: true },
|
|
273
|
+
}),
|
|
274
|
+
prisma.user.count({ where }),
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
data: users,
|
|
279
|
+
pagination: {
|
|
280
|
+
total,
|
|
281
|
+
page: skip ? Math.floor(skip / (take || 20)) + 1 : 1,
|
|
282
|
+
limit: take || 20,
|
|
283
|
+
totalPages: Math.ceil(total / (take || 20)),
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Search users
|
|
289
|
+
async search(query: string, role?: UserRole) {
|
|
290
|
+
return prisma.user.findMany({
|
|
291
|
+
where: {
|
|
292
|
+
AND: [
|
|
293
|
+
role ? { role } : {},
|
|
294
|
+
{
|
|
295
|
+
OR: [
|
|
296
|
+
{ email: { contains: query, mode: 'insensitive' } },
|
|
297
|
+
{ profile: { firstName: { contains: query, mode: 'insensitive' } } },
|
|
298
|
+
{ profile: { lastName: { contains: query, mode: 'insensitive' } } },
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
include: { profile: true },
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Update with upsert for profile
|
|
308
|
+
async updateProfile(
|
|
309
|
+
userId: string,
|
|
310
|
+
data: { firstName?: string; lastName?: string; bio?: string }
|
|
311
|
+
) {
|
|
312
|
+
return prisma.user.update({
|
|
313
|
+
where: { id: userId },
|
|
314
|
+
data: {
|
|
315
|
+
profile: {
|
|
316
|
+
upsert: {
|
|
317
|
+
create: {
|
|
318
|
+
firstName: data.firstName || '',
|
|
319
|
+
lastName: data.lastName || '',
|
|
320
|
+
bio: data.bio,
|
|
321
|
+
},
|
|
322
|
+
update: data,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
include: { profile: true },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Delete with cascade
|
|
331
|
+
async delete(userId: string) {
|
|
332
|
+
return prisma.user.delete({
|
|
333
|
+
where: { id: userId },
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Get user with organizations
|
|
338
|
+
async getUserWithOrganizations(userId: string): Promise<UserWithOrganizations | null> {
|
|
339
|
+
return prisma.user.findUnique({
|
|
340
|
+
where: { id: userId },
|
|
341
|
+
include: {
|
|
342
|
+
memberships: {
|
|
343
|
+
include: {
|
|
344
|
+
organization: true,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### 3. Transactions
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
// src/services/order.service.ts
|
|
357
|
+
import { PrismaClient, Prisma } from '@prisma/client';
|
|
358
|
+
|
|
359
|
+
const prisma = new PrismaClient();
|
|
360
|
+
|
|
361
|
+
interface OrderItem {
|
|
362
|
+
productId: string;
|
|
363
|
+
quantity: number;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export class OrderService {
|
|
367
|
+
// Interactive transaction for creating order
|
|
368
|
+
async createOrder(userId: string, items: OrderItem[]) {
|
|
369
|
+
return prisma.$transaction(async (tx) => {
|
|
370
|
+
// Validate and get products
|
|
371
|
+
const productIds = items.map((i) => i.productId);
|
|
372
|
+
const products = await tx.product.findMany({
|
|
373
|
+
where: { id: { in: productIds } },
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (products.length !== items.length) {
|
|
377
|
+
throw new Error('Some products not found');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check stock and calculate total
|
|
381
|
+
let total = new Prisma.Decimal(0);
|
|
382
|
+
const orderItems: Prisma.OrderItemCreateManyOrderInput[] = [];
|
|
37
383
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
384
|
+
for (const item of items) {
|
|
385
|
+
const product = products.find((p) => p.id === item.productId)!;
|
|
386
|
+
|
|
387
|
+
if (product.stock < item.quantity) {
|
|
388
|
+
throw new Error(`Insufficient stock for ${product.name}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Update stock
|
|
392
|
+
await tx.product.update({
|
|
393
|
+
where: { id: product.id },
|
|
394
|
+
data: { stock: { decrement: item.quantity } },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const itemTotal = product.price.mul(item.quantity);
|
|
398
|
+
total = total.add(itemTotal);
|
|
399
|
+
|
|
400
|
+
orderItems.push({
|
|
401
|
+
productId: product.id,
|
|
402
|
+
quantity: item.quantity,
|
|
403
|
+
price: product.price,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Create order with items
|
|
408
|
+
return tx.order.create({
|
|
409
|
+
data: {
|
|
410
|
+
userId,
|
|
411
|
+
total,
|
|
412
|
+
items: {
|
|
413
|
+
createMany: { data: orderItems },
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
include: {
|
|
417
|
+
items: {
|
|
418
|
+
include: { product: true },
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Sequential transaction for batch operations
|
|
426
|
+
async batchUpdateOrderStatus(
|
|
427
|
+
orderIds: string[],
|
|
428
|
+
status: 'CONFIRMED' | 'SHIPPED' | 'DELIVERED'
|
|
429
|
+
) {
|
|
430
|
+
const operations = orderIds.map((id) =>
|
|
431
|
+
prisma.order.update({
|
|
432
|
+
where: { id },
|
|
433
|
+
data: { status },
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
return prisma.$transaction(operations);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Cancel order with stock restoration
|
|
441
|
+
async cancelOrder(orderId: string) {
|
|
442
|
+
return prisma.$transaction(async (tx) => {
|
|
443
|
+
const order = await tx.order.findUnique({
|
|
444
|
+
where: { id: orderId },
|
|
445
|
+
include: { items: true },
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (!order) {
|
|
449
|
+
throw new Error('Order not found');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (order.status !== 'PENDING') {
|
|
453
|
+
throw new Error('Only pending orders can be cancelled');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Restore stock
|
|
457
|
+
for (const item of order.items) {
|
|
458
|
+
await tx.product.update({
|
|
459
|
+
where: { id: item.productId },
|
|
460
|
+
data: { stock: { increment: item.quantity } },
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Update order status
|
|
465
|
+
return tx.order.update({
|
|
466
|
+
where: { id: orderId },
|
|
467
|
+
data: { status: 'CANCELLED' },
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 4. Migrations
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Generate migration from schema changes
|
|
478
|
+
npx prisma migrate dev --name add_user_profile
|
|
479
|
+
|
|
480
|
+
# Apply migrations in production
|
|
481
|
+
npx prisma migrate deploy
|
|
482
|
+
|
|
483
|
+
# Reset database (development only)
|
|
484
|
+
npx prisma migrate reset
|
|
485
|
+
|
|
486
|
+
# Generate Prisma Client
|
|
487
|
+
npx prisma generate
|
|
488
|
+
|
|
489
|
+
# Open Prisma Studio
|
|
490
|
+
npx prisma studio
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// prisma/seed.ts
|
|
495
|
+
import { PrismaClient } from '@prisma/client';
|
|
496
|
+
import { hash } from 'bcrypt';
|
|
497
|
+
|
|
498
|
+
const prisma = new PrismaClient();
|
|
499
|
+
|
|
500
|
+
async function main() {
|
|
501
|
+
// Create admin user
|
|
502
|
+
const adminPassword = await hash('admin123', 12);
|
|
503
|
+
const admin = await prisma.user.upsert({
|
|
504
|
+
where: { email: 'admin@example.com' },
|
|
505
|
+
update: {},
|
|
506
|
+
create: {
|
|
507
|
+
email: 'admin@example.com',
|
|
508
|
+
password: adminPassword,
|
|
509
|
+
role: 'ADMIN',
|
|
510
|
+
profile: {
|
|
511
|
+
create: {
|
|
512
|
+
firstName: 'Admin',
|
|
513
|
+
lastName: 'User',
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Create categories
|
|
520
|
+
const categories = await Promise.all(
|
|
521
|
+
['Technology', 'Design', 'Business'].map((name) =>
|
|
522
|
+
prisma.category.upsert({
|
|
523
|
+
where: { slug: name.toLowerCase() },
|
|
524
|
+
update: {},
|
|
525
|
+
create: {
|
|
526
|
+
name,
|
|
527
|
+
slug: name.toLowerCase(),
|
|
528
|
+
},
|
|
529
|
+
})
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// Create products
|
|
534
|
+
const products = await prisma.product.createMany({
|
|
535
|
+
data: [
|
|
536
|
+
{ name: 'Product 1', price: 29.99, stock: 100 },
|
|
537
|
+
{ name: 'Product 2', price: 49.99, stock: 50 },
|
|
538
|
+
{ name: 'Product 3', price: 99.99, stock: 25 },
|
|
539
|
+
],
|
|
540
|
+
skipDuplicates: true,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
console.log({ admin, categories, products });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
main()
|
|
547
|
+
.catch((e) => {
|
|
548
|
+
console.error(e);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
})
|
|
551
|
+
.finally(async () => {
|
|
552
|
+
await prisma.$disconnect();
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### 5. Advanced Queries
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
// src/repositories/post.repository.ts
|
|
560
|
+
import { PrismaClient, Prisma } from '@prisma/client';
|
|
561
|
+
|
|
562
|
+
const prisma = new PrismaClient();
|
|
563
|
+
|
|
564
|
+
export class PostRepository {
|
|
565
|
+
// Full-text search
|
|
566
|
+
async searchPosts(query: string) {
|
|
567
|
+
return prisma.post.findMany({
|
|
568
|
+
where: {
|
|
569
|
+
OR: [
|
|
570
|
+
{ title: { search: query } },
|
|
571
|
+
{ content: { search: query } },
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
orderBy: {
|
|
575
|
+
_relevance: {
|
|
576
|
+
fields: ['title', 'content'],
|
|
577
|
+
search: query,
|
|
578
|
+
sort: 'desc',
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Aggregations
|
|
585
|
+
async getPostStats() {
|
|
586
|
+
return prisma.post.aggregate({
|
|
587
|
+
_count: { id: true },
|
|
588
|
+
_avg: { id: true },
|
|
589
|
+
where: { published: true },
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Group by
|
|
594
|
+
async getPostCountByAuthor() {
|
|
595
|
+
return prisma.post.groupBy({
|
|
596
|
+
by: ['authorId'],
|
|
597
|
+
_count: { id: true },
|
|
598
|
+
having: {
|
|
599
|
+
id: { _count: { gt: 5 } },
|
|
600
|
+
},
|
|
601
|
+
orderBy: {
|
|
602
|
+
_count: { id: 'desc' },
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Raw queries
|
|
608
|
+
async getTopAuthors(limit: number) {
|
|
609
|
+
return prisma.$queryRaw<Array<{ author_id: string; post_count: bigint }>>`
|
|
610
|
+
SELECT author_id, COUNT(*) as post_count
|
|
611
|
+
FROM posts
|
|
612
|
+
WHERE published = true
|
|
613
|
+
GROUP BY author_id
|
|
614
|
+
ORDER BY post_count DESC
|
|
615
|
+
LIMIT ${limit}
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Complex filtering
|
|
620
|
+
async findPosts(filters: {
|
|
621
|
+
categoryIds?: string[];
|
|
622
|
+
tagIds?: string[];
|
|
623
|
+
authorId?: string;
|
|
624
|
+
published?: boolean;
|
|
625
|
+
search?: string;
|
|
626
|
+
dateRange?: { start: Date; end: Date };
|
|
627
|
+
}) {
|
|
628
|
+
const where: Prisma.PostWhereInput = {};
|
|
629
|
+
|
|
630
|
+
if (filters.categoryIds?.length) {
|
|
631
|
+
where.categories = {
|
|
632
|
+
some: { id: { in: filters.categoryIds } },
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (filters.tagIds?.length) {
|
|
637
|
+
where.tags = {
|
|
638
|
+
some: { id: { in: filters.tagIds } },
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (filters.authorId) {
|
|
643
|
+
where.authorId = filters.authorId;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (filters.published !== undefined) {
|
|
647
|
+
where.published = filters.published;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (filters.search) {
|
|
651
|
+
where.OR = [
|
|
652
|
+
{ title: { contains: filters.search, mode: 'insensitive' } },
|
|
653
|
+
{ content: { contains: filters.search, mode: 'insensitive' } },
|
|
654
|
+
];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (filters.dateRange) {
|
|
658
|
+
where.createdAt = {
|
|
659
|
+
gte: filters.dateRange.start,
|
|
660
|
+
lte: filters.dateRange.end,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return prisma.post.findMany({
|
|
665
|
+
where,
|
|
666
|
+
include: {
|
|
667
|
+
author: { include: { profile: true } },
|
|
668
|
+
categories: true,
|
|
669
|
+
tags: true,
|
|
670
|
+
},
|
|
671
|
+
orderBy: { createdAt: 'desc' },
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### 6. Connection and Configuration
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
// src/lib/prisma.ts
|
|
681
|
+
import { PrismaClient } from '@prisma/client';
|
|
682
|
+
|
|
683
|
+
declare global {
|
|
684
|
+
var prisma: PrismaClient | undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const prismaClientSingleton = () => {
|
|
688
|
+
return new PrismaClient({
|
|
689
|
+
log: process.env.NODE_ENV === 'development'
|
|
690
|
+
? ['query', 'error', 'warn']
|
|
691
|
+
: ['error'],
|
|
692
|
+
});
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
export const prisma = globalThis.prisma ?? prismaClientSingleton();
|
|
696
|
+
|
|
697
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
698
|
+
globalThis.prisma = prisma;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// With middleware for soft delete
|
|
702
|
+
const prismaWithSoftDelete = new PrismaClient().$extends({
|
|
703
|
+
query: {
|
|
704
|
+
user: {
|
|
705
|
+
async findMany({ model, operation, args, query }) {
|
|
706
|
+
args.where = { ...args.where, deletedAt: null };
|
|
707
|
+
return query(args);
|
|
708
|
+
},
|
|
709
|
+
async delete({ model, operation, args, query }) {
|
|
710
|
+
return prisma.user.update({
|
|
711
|
+
...args,
|
|
712
|
+
data: { deletedAt: new Date() },
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
},
|
|
42
717
|
});
|
|
718
|
+
```
|
|
43
719
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
720
|
+
## Use Cases
|
|
721
|
+
|
|
722
|
+
### Pagination with Cursor
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
async function getPaginatedPosts(cursor?: string, take: number = 20) {
|
|
726
|
+
const posts = await prisma.post.findMany({
|
|
727
|
+
take: take + 1,
|
|
728
|
+
...(cursor && {
|
|
729
|
+
skip: 1,
|
|
730
|
+
cursor: { id: cursor },
|
|
731
|
+
}),
|
|
732
|
+
orderBy: { createdAt: 'desc' },
|
|
733
|
+
include: { author: true },
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const hasMore = posts.length > take;
|
|
737
|
+
const data = hasMore ? posts.slice(0, -1) : posts;
|
|
738
|
+
const nextCursor = hasMore ? data[data.length - 1].id : null;
|
|
739
|
+
|
|
740
|
+
return { data, nextCursor, hasMore };
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### Optimistic Concurrency
|
|
745
|
+
|
|
746
|
+
```typescript
|
|
747
|
+
async function updateWithVersion(postId: string, data: any, version: number) {
|
|
748
|
+
const result = await prisma.post.updateMany({
|
|
749
|
+
where: {
|
|
750
|
+
id: postId,
|
|
751
|
+
version: version,
|
|
752
|
+
},
|
|
753
|
+
data: {
|
|
754
|
+
...data,
|
|
755
|
+
version: { increment: 1 },
|
|
756
|
+
},
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
if (result.count === 0) {
|
|
760
|
+
throw new Error('Concurrent modification detected');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return prisma.post.findUnique({ where: { id: postId } });
|
|
764
|
+
}
|
|
49
765
|
```
|
|
50
766
|
|
|
51
767
|
## Best Practices
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- Use
|
|
768
|
+
|
|
769
|
+
### Do's
|
|
770
|
+
|
|
771
|
+
- Use type-safe queries with generated types
|
|
772
|
+
- Implement proper error handling
|
|
773
|
+
- Use transactions for multi-operation writes
|
|
774
|
+
- Create indexes for frequently queried fields
|
|
775
|
+
- Use select/include to limit returned data
|
|
776
|
+
- Implement pagination for large datasets
|
|
777
|
+
- Use connection pooling in production
|
|
778
|
+
- Run migrations in CI/CD pipelines
|
|
779
|
+
- Seed development databases
|
|
780
|
+
- Use Prisma Studio for debugging
|
|
781
|
+
|
|
782
|
+
### Don'ts
|
|
783
|
+
|
|
784
|
+
- Don't expose Prisma Client directly in APIs
|
|
785
|
+
- Don't skip migrations in production
|
|
786
|
+
- Don't use raw queries unless necessary
|
|
787
|
+
- Don't ignore relation loading (N+1)
|
|
788
|
+
- Don't hardcode connection strings
|
|
789
|
+
- Don't skip input validation
|
|
790
|
+
- Don't use implicit many-to-many for complex relations
|
|
791
|
+
- Don't ignore transaction isolation levels
|
|
792
|
+
- Don't skip database backups
|
|
793
|
+
- Don't use synchronous operations
|
|
794
|
+
|
|
795
|
+
## References
|
|
796
|
+
|
|
797
|
+
- [Prisma Documentation](https://www.prisma.io/docs)
|
|
798
|
+
- [Prisma Client API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference)
|
|
799
|
+
- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
|
|
800
|
+
- [Prisma Best Practices](https://www.prisma.io/docs/guides)
|
|
801
|
+
- [Prisma Blog](https://www.prisma.io/blog)
|