omgkit 2.2.0 → 2.3.1
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/README.md +3 -3
- package/package.json +1 -1
- package/plugin/skills/databases/database-management/SKILL.md +288 -0
- package/plugin/skills/databases/database-migration/SKILL.md +285 -0
- package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/databases/supabase/SKILL.md +283 -0
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,41 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Prisma ORM
|
|
4
|
-
category: databases
|
|
5
|
-
triggers:
|
|
6
|
-
- prisma
|
|
7
|
-
- prisma client
|
|
8
|
-
- prisma migrate
|
|
9
|
-
- prisma schema
|
|
10
|
-
- orm
|
|
11
|
-
- type-safe database
|
|
2
|
+
name: Developing with Prisma
|
|
3
|
+
description: The agent implements Prisma ORM for type-safe database access with schema design, migrations, and queries. Use when building database layers, designing relational schemas, implementing type-safe queries, or managing database migrations.
|
|
12
4
|
---
|
|
13
5
|
|
|
14
|
-
# Prisma
|
|
6
|
+
# Developing with Prisma
|
|
15
7
|
|
|
16
|
-
|
|
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
|
|
8
|
+
## Quick Start
|
|
33
9
|
|
|
34
10
|
```prisma
|
|
35
11
|
// prisma/schema.prisma
|
|
36
12
|
generator client {
|
|
37
|
-
provider
|
|
38
|
-
previewFeatures = ["fullTextSearch", "fullTextIndex"]
|
|
13
|
+
provider = "prisma-client-js"
|
|
39
14
|
}
|
|
40
15
|
|
|
41
16
|
datasource db {
|
|
@@ -43,759 +18,93 @@ datasource db {
|
|
|
43
18
|
url = env("DATABASE_URL")
|
|
44
19
|
}
|
|
45
20
|
|
|
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
|
|
62
21
|
model User {
|
|
63
22
|
id String @id @default(cuid())
|
|
64
23
|
email String @unique
|
|
65
|
-
|
|
66
|
-
role UserRole @default(USER)
|
|
67
|
-
isActive Boolean @default(true)
|
|
24
|
+
posts Post[]
|
|
68
25
|
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
26
|
@@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")
|
|
95
27
|
}
|
|
96
28
|
|
|
97
|
-
// One-to-many relation
|
|
98
29
|
model Post {
|
|
99
|
-
id
|
|
100
|
-
title
|
|
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
|
-
|
|
30
|
+
id String @id @default(cuid())
|
|
31
|
+
title String
|
|
108
32
|
authorId String
|
|
109
|
-
author User @relation(fields: [authorId], references: [id]
|
|
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")
|
|
33
|
+
author User @relation(fields: [authorId], references: [id])
|
|
211
34
|
}
|
|
212
35
|
```
|
|
213
36
|
|
|
214
|
-
### 2. Type-Safe Queries
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
// src/repositories/user.repository.ts
|
|
218
|
-
import { PrismaClient, Prisma, User, UserRole } from '@prisma/client';
|
|
219
|
-
|
|
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[] = [];
|
|
383
|
-
|
|
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
37
|
```bash
|
|
477
|
-
|
|
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
|
|
38
|
+
npx prisma migrate dev --name init
|
|
487
39
|
npx prisma generate
|
|
488
|
-
|
|
489
|
-
# Open Prisma Studio
|
|
490
|
-
npx prisma studio
|
|
491
40
|
```
|
|
492
41
|
|
|
493
|
-
|
|
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
|
-
});
|
|
42
|
+
## Features
|
|
542
43
|
|
|
543
|
-
|
|
544
|
-
|
|
44
|
+
| Feature | Description | Guide |
|
|
45
|
+
|---------|-------------|-------|
|
|
46
|
+
| Schema Design | Declarative data modeling with relations | Define models, relations, indexes in schema.prisma |
|
|
47
|
+
| Type-Safe Queries | Auto-generated TypeScript types | Use `findMany`, `findUnique`, `create`, `update` |
|
|
48
|
+
| Migrations | Version-controlled schema changes | `prisma migrate dev` for development, `deploy` for production |
|
|
49
|
+
| Relations | One-to-one, one-to-many, many-to-many | Use `include` or `select` to load related data |
|
|
50
|
+
| Transactions | ACID operations across multiple queries | Use `$transaction` for atomic operations |
|
|
51
|
+
| Raw Queries | Execute raw SQL when needed | Use `$queryRaw` for complex queries |
|
|
545
52
|
|
|
546
|
-
|
|
547
|
-
.catch((e) => {
|
|
548
|
-
console.error(e);
|
|
549
|
-
process.exit(1);
|
|
550
|
-
})
|
|
551
|
-
.finally(async () => {
|
|
552
|
-
await prisma.$disconnect();
|
|
553
|
-
});
|
|
554
|
-
```
|
|
53
|
+
## Common Patterns
|
|
555
54
|
|
|
556
|
-
###
|
|
55
|
+
### Repository with Pagination
|
|
557
56
|
|
|
558
57
|
```typescript
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
}
|
|
58
|
+
async function findUsers(page = 1, limit = 20, where?: Prisma.UserWhereInput) {
|
|
59
|
+
const [data, total] = await prisma.$transaction([
|
|
60
|
+
prisma.user.findMany({ where, skip: (page - 1) * limit, take: limit, include: { profile: true } }),
|
|
61
|
+
prisma.user.count({ where }),
|
|
62
|
+
]);
|
|
63
|
+
return { data, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } };
|
|
674
64
|
}
|
|
675
65
|
```
|
|
676
66
|
|
|
677
|
-
###
|
|
67
|
+
### Interactive Transaction
|
|
678
68
|
|
|
679
69
|
```typescript
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
70
|
+
async function createOrder(userId: string, items: { productId: string; qty: number }[]) {
|
|
71
|
+
return prisma.$transaction(async (tx) => {
|
|
72
|
+
let total = 0;
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
const product = await tx.product.update({
|
|
75
|
+
where: { id: item.productId },
|
|
76
|
+
data: { stock: { decrement: item.qty } },
|
|
77
|
+
});
|
|
78
|
+
if (product.stock < 0) throw new Error(`Insufficient stock: ${product.name}`);
|
|
79
|
+
total += product.price * item.qty;
|
|
80
|
+
}
|
|
81
|
+
return tx.order.create({ data: { userId, total, items: { create: items } } });
|
|
692
82
|
});
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
export const prisma = globalThis.prisma ?? prismaClientSingleton();
|
|
696
|
-
|
|
697
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
698
|
-
globalThis.prisma = prisma;
|
|
699
83
|
}
|
|
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
|
-
},
|
|
717
|
-
});
|
|
718
84
|
```
|
|
719
85
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
### Pagination with Cursor
|
|
86
|
+
### Cursor-Based Pagination
|
|
723
87
|
|
|
724
88
|
```typescript
|
|
725
|
-
async function getPaginatedPosts(cursor?: string, take
|
|
89
|
+
async function getPaginatedPosts(cursor?: string, take = 20) {
|
|
726
90
|
const posts = await prisma.post.findMany({
|
|
727
91
|
take: take + 1,
|
|
728
|
-
...(cursor && {
|
|
729
|
-
skip: 1,
|
|
730
|
-
cursor: { id: cursor },
|
|
731
|
-
}),
|
|
92
|
+
...(cursor && { skip: 1, cursor: { id: cursor } }),
|
|
732
93
|
orderBy: { createdAt: 'desc' },
|
|
733
|
-
include: { author: true },
|
|
734
94
|
});
|
|
735
|
-
|
|
736
95
|
const hasMore = posts.length > take;
|
|
737
|
-
|
|
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 } });
|
|
96
|
+
return { data: hasMore ? posts.slice(0, -1) : posts, nextCursor: hasMore ? posts[take - 1].id : null };
|
|
764
97
|
}
|
|
765
98
|
```
|
|
766
99
|
|
|
767
100
|
## Best Practices
|
|
768
101
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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)
|
|
102
|
+
| Do | Avoid |
|
|
103
|
+
|----|-------|
|
|
104
|
+
| Use `select` to fetch only needed fields | Exposing Prisma Client directly in APIs |
|
|
105
|
+
| Create indexes for frequently queried fields | Skipping migrations in production |
|
|
106
|
+
| Use transactions for multi-table operations | Ignoring N+1 query problems |
|
|
107
|
+
| Run migrations in CI/CD pipelines | Hardcoding connection strings |
|
|
108
|
+
| Use connection pooling in production | Using raw queries unless necessary |
|
|
109
|
+
| Validate input before database operations | Using implicit many-to-many for complex joins |
|
|
110
|
+
| Seed development databases consistently | Ignoring transaction isolation levels |
|