@malamute/ai-rules 1.0.0 → 1.3.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/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -209,3 +209,55 @@ export default function Page() {}
|
|
|
209
209
|
- Inline styles (use CSS modules or Tailwind)
|
|
210
210
|
- Anonymous components
|
|
211
211
|
- Props spreading without type safety
|
|
212
|
+
|
|
213
|
+
## Error and Loading UI
|
|
214
|
+
|
|
215
|
+
### Error Boundary (error.tsx)
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
'use client';
|
|
219
|
+
|
|
220
|
+
interface ErrorProps {
|
|
221
|
+
error: Error & { digest?: string };
|
|
222
|
+
reset: () => void;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default function Error({ error, reset }: ErrorProps) {
|
|
226
|
+
return (
|
|
227
|
+
<div className="error-container">
|
|
228
|
+
<h2>Something went wrong!</h2>
|
|
229
|
+
<p>{error.message}</p>
|
|
230
|
+
<button onClick={reset}>Try again</button>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Loading State (loading.tsx)
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
export default function Loading() {
|
|
240
|
+
return (
|
|
241
|
+
<div className="loading-container">
|
|
242
|
+
<Spinner />
|
|
243
|
+
<p>Loading...</p>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Not Found (not-found.tsx)
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import Link from 'next/link';
|
|
253
|
+
|
|
254
|
+
export default function NotFound() {
|
|
255
|
+
return (
|
|
256
|
+
<div>
|
|
257
|
+
<h2>Not Found</h2>
|
|
258
|
+
<p>Could not find the requested resource.</p>
|
|
259
|
+
<Link href="/">Return Home</Link>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "app/**/*.tsx"
|
|
4
|
+
- "app/**/*.ts"
|
|
5
|
+
- "**/actions.ts"
|
|
6
|
+
- "**/actions/*.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Data Fetching & Server Actions
|
|
10
|
+
|
|
11
|
+
## Server Components - Data Fetching
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// app/users/page.tsx
|
|
15
|
+
async function getUsers(): Promise<User[]> {
|
|
16
|
+
const response = await fetch('https://api.example.com/users', {
|
|
17
|
+
cache: 'no-store', // Dynamic - always fresh
|
|
18
|
+
// cache: 'force-cache', // Static (default)
|
|
19
|
+
// next: { revalidate: 60 }, // ISR - revalidate every 60s
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error('Failed to fetch users');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default async function UsersPage() {
|
|
30
|
+
const users = await getUsers();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ul>
|
|
34
|
+
{users.map((user) => (
|
|
35
|
+
<li key={user.id}>{user.name}</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Cache Options
|
|
43
|
+
|
|
44
|
+
| Option | Behavior |
|
|
45
|
+
|--------|----------|
|
|
46
|
+
| `cache: 'force-cache'` | Static, cached indefinitely (default) |
|
|
47
|
+
| `cache: 'no-store'` | Dynamic, never cached |
|
|
48
|
+
| `next: { revalidate: N }` | ISR, revalidate every N seconds |
|
|
49
|
+
| `next: { tags: ['users'] }` | Tagged for on-demand revalidation |
|
|
50
|
+
|
|
51
|
+
## Server Actions
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// app/users/actions.ts
|
|
55
|
+
'use server';
|
|
56
|
+
|
|
57
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
58
|
+
import { redirect } from 'next/navigation';
|
|
59
|
+
import { z } from 'zod';
|
|
60
|
+
|
|
61
|
+
const CreateUserSchema = z.object({
|
|
62
|
+
name: z.string().min(1),
|
|
63
|
+
email: z.string().email(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export async function createUser(formData: FormData) {
|
|
67
|
+
// Validate
|
|
68
|
+
const parsed = CreateUserSchema.safeParse({
|
|
69
|
+
name: formData.get('name'),
|
|
70
|
+
email: formData.get('email'),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
return { error: 'Invalid data' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create
|
|
78
|
+
await db.user.create({ data: parsed.data });
|
|
79
|
+
|
|
80
|
+
// Revalidate and redirect
|
|
81
|
+
revalidatePath('/users');
|
|
82
|
+
redirect('/users');
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Form with Server Action
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// app/users/_components/user-form.tsx
|
|
90
|
+
'use client';
|
|
91
|
+
|
|
92
|
+
import { useActionState } from 'react';
|
|
93
|
+
import { createUser } from '../actions';
|
|
94
|
+
|
|
95
|
+
const initialState = { error: null };
|
|
96
|
+
|
|
97
|
+
export function UserForm() {
|
|
98
|
+
const [state, formAction, isPending] = useActionState(
|
|
99
|
+
createUser,
|
|
100
|
+
initialState
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<form action={formAction}>
|
|
105
|
+
<input name="name" required disabled={isPending} />
|
|
106
|
+
<input name="email" type="email" required disabled={isPending} />
|
|
107
|
+
|
|
108
|
+
{state.error && <p className="error">{state.error}</p>}
|
|
109
|
+
|
|
110
|
+
<button type="submit" disabled={isPending}>
|
|
111
|
+
{isPending ? 'Creating...' : 'Create User'}
|
|
112
|
+
</button>
|
|
113
|
+
</form>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Optimistic Updates
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
'use client';
|
|
122
|
+
|
|
123
|
+
import { useOptimistic } from 'react';
|
|
124
|
+
import { addItem } from './actions';
|
|
125
|
+
|
|
126
|
+
export function ItemList({ items }: { items: Item[] }) {
|
|
127
|
+
const [optimisticItems, addOptimisticItem] = useOptimistic(
|
|
128
|
+
items,
|
|
129
|
+
(state, newItem: Item) => [...state, newItem]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
async function handleAdd(formData: FormData) {
|
|
133
|
+
const newItem = {
|
|
134
|
+
id: crypto.randomUUID(),
|
|
135
|
+
name: formData.get('name') as string,
|
|
136
|
+
pending: true,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
addOptimisticItem(newItem);
|
|
140
|
+
await addItem(formData);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<form action={handleAdd}>
|
|
146
|
+
<input name="name" />
|
|
147
|
+
<button type="submit">Add</button>
|
|
148
|
+
</form>
|
|
149
|
+
|
|
150
|
+
<ul>
|
|
151
|
+
{optimisticItems.map((item) => (
|
|
152
|
+
<li key={item.id} style={{ opacity: item.pending ? 0.5 : 1 }}>
|
|
153
|
+
{item.name}
|
|
154
|
+
</li>
|
|
155
|
+
))}
|
|
156
|
+
</ul>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Parallel Data Fetching
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
// Parallel - faster
|
|
166
|
+
export default async function Page() {
|
|
167
|
+
const [users, products] = await Promise.all([
|
|
168
|
+
getUsers(),
|
|
169
|
+
getProducts(),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
return (/* ... */);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Sequential - slower, avoid
|
|
176
|
+
export default async function Page() {
|
|
177
|
+
const users = await getUsers();
|
|
178
|
+
const products = await getProducts(); // Waits for users
|
|
179
|
+
return (/* ... */);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Data Fetching with Suspense
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { Suspense } from 'react';
|
|
187
|
+
|
|
188
|
+
export default function Page() {
|
|
189
|
+
return (
|
|
190
|
+
<div>
|
|
191
|
+
<h1>Dashboard</h1>
|
|
192
|
+
|
|
193
|
+
<Suspense fallback={<UsersSkeleton />}>
|
|
194
|
+
<UsersSection />
|
|
195
|
+
</Suspense>
|
|
196
|
+
|
|
197
|
+
<Suspense fallback={<ProductsSkeleton />}>
|
|
198
|
+
<ProductsSection />
|
|
199
|
+
</Suspense>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function UsersSection() {
|
|
205
|
+
const users = await getUsers();
|
|
206
|
+
return <UserList users={users} />;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Revalidation Patterns
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
'use server';
|
|
214
|
+
|
|
215
|
+
// Path-based
|
|
216
|
+
revalidatePath('/users'); // Revalidate specific path
|
|
217
|
+
revalidatePath('/users', 'layout'); // Revalidate layout and children
|
|
218
|
+
|
|
219
|
+
// Tag-based
|
|
220
|
+
revalidateTag('users'); // Revalidate all with this tag
|
|
221
|
+
|
|
222
|
+
// Usage with tags
|
|
223
|
+
await fetch('/api/users', {
|
|
224
|
+
next: { tags: ['users'] }
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Error Handling in Server Actions
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
'use server';
|
|
232
|
+
|
|
233
|
+
export async function createUser(formData: FormData) {
|
|
234
|
+
try {
|
|
235
|
+
const user = await db.user.create({
|
|
236
|
+
data: { name: formData.get('name') as string }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
revalidatePath('/users');
|
|
240
|
+
return { success: true, user };
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// Log for debugging
|
|
243
|
+
console.error('Failed to create user:', error);
|
|
244
|
+
|
|
245
|
+
// Return user-friendly error
|
|
246
|
+
return { success: false, error: 'Failed to create user' };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/prisma/**"
|
|
4
|
+
- "**/db/**"
|
|
5
|
+
- "**/lib/db.ts"
|
|
6
|
+
- "**/lib/prisma.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Next.js Database (Prisma)
|
|
10
|
+
|
|
11
|
+
## Prisma Setup
|
|
12
|
+
|
|
13
|
+
### Client Singleton
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// lib/prisma.ts
|
|
17
|
+
import { PrismaClient } from '@prisma/client';
|
|
18
|
+
|
|
19
|
+
const globalForPrisma = globalThis as unknown as {
|
|
20
|
+
prisma: PrismaClient | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
|
24
|
+
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
28
|
+
globalForPrisma.prisma = prisma;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Schema
|
|
33
|
+
|
|
34
|
+
```prisma
|
|
35
|
+
// prisma/schema.prisma
|
|
36
|
+
generator client {
|
|
37
|
+
provider = "prisma-client-js"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
datasource db {
|
|
41
|
+
provider = "postgresql"
|
|
42
|
+
url = env("DATABASE_URL")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
model User {
|
|
46
|
+
id String @id @default(cuid())
|
|
47
|
+
email String @unique
|
|
48
|
+
name String?
|
|
49
|
+
role Role @default(USER)
|
|
50
|
+
posts Post[]
|
|
51
|
+
createdAt DateTime @default(now())
|
|
52
|
+
updatedAt DateTime @updatedAt
|
|
53
|
+
|
|
54
|
+
@@index([email])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
model Post {
|
|
58
|
+
id String @id @default(cuid())
|
|
59
|
+
title String
|
|
60
|
+
slug String @unique
|
|
61
|
+
content String?
|
|
62
|
+
published Boolean @default(false)
|
|
63
|
+
author User @relation(fields: [authorId], references: [id])
|
|
64
|
+
authorId String
|
|
65
|
+
createdAt DateTime @default(now())
|
|
66
|
+
updatedAt DateTime @updatedAt
|
|
67
|
+
|
|
68
|
+
@@index([authorId])
|
|
69
|
+
@@index([slug])
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
enum Role {
|
|
73
|
+
USER
|
|
74
|
+
ADMIN
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Query Patterns
|
|
79
|
+
|
|
80
|
+
### Server Components
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// app/users/page.tsx
|
|
84
|
+
import { prisma } from '@/lib/prisma';
|
|
85
|
+
|
|
86
|
+
export default async function UsersPage() {
|
|
87
|
+
const users = await prisma.user.findMany({
|
|
88
|
+
select: {
|
|
89
|
+
id: true,
|
|
90
|
+
name: true,
|
|
91
|
+
email: true,
|
|
92
|
+
_count: { select: { posts: true } },
|
|
93
|
+
},
|
|
94
|
+
orderBy: { createdAt: 'desc' },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<ul>
|
|
99
|
+
{users.map(user => (
|
|
100
|
+
<li key={user.id}>
|
|
101
|
+
{user.name} - {user._count.posts} posts
|
|
102
|
+
</li>
|
|
103
|
+
))}
|
|
104
|
+
</ul>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### With Pagination
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// app/posts/page.tsx
|
|
113
|
+
import { prisma } from '@/lib/prisma';
|
|
114
|
+
|
|
115
|
+
type Props = {
|
|
116
|
+
searchParams: Promise<{ page?: string; limit?: string }>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default async function PostsPage({ searchParams }: Props) {
|
|
120
|
+
const { page = '1', limit = '10' } = await searchParams;
|
|
121
|
+
const pageNum = parseInt(page);
|
|
122
|
+
const limitNum = parseInt(limit);
|
|
123
|
+
|
|
124
|
+
const [posts, total] = await Promise.all([
|
|
125
|
+
prisma.post.findMany({
|
|
126
|
+
where: { published: true },
|
|
127
|
+
include: { author: { select: { name: true } } },
|
|
128
|
+
skip: (pageNum - 1) * limitNum,
|
|
129
|
+
take: limitNum,
|
|
130
|
+
orderBy: { createdAt: 'desc' },
|
|
131
|
+
}),
|
|
132
|
+
prisma.post.count({ where: { published: true } }),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const totalPages = Math.ceil(total / limitNum);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<>
|
|
139
|
+
<PostList posts={posts} />
|
|
140
|
+
<Pagination
|
|
141
|
+
currentPage={pageNum}
|
|
142
|
+
totalPages={totalPages}
|
|
143
|
+
/>
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Server Actions
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// app/posts/actions.ts
|
|
153
|
+
'use server';
|
|
154
|
+
|
|
155
|
+
import { prisma } from '@/lib/prisma';
|
|
156
|
+
import { auth } from '@/auth';
|
|
157
|
+
import { revalidatePath } from 'next/cache';
|
|
158
|
+
import { z } from 'zod';
|
|
159
|
+
|
|
160
|
+
const createPostSchema = z.object({
|
|
161
|
+
title: z.string().min(1).max(200),
|
|
162
|
+
content: z.string().optional(),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export async function createPost(formData: FormData) {
|
|
166
|
+
const session = await auth();
|
|
167
|
+
if (!session) throw new Error('Unauthorized');
|
|
168
|
+
|
|
169
|
+
const data = createPostSchema.parse({
|
|
170
|
+
title: formData.get('title'),
|
|
171
|
+
content: formData.get('content'),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const slug = data.title
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
177
|
+
.replace(/(^-|-$)/g, '');
|
|
178
|
+
|
|
179
|
+
const post = await prisma.post.create({
|
|
180
|
+
data: {
|
|
181
|
+
...data,
|
|
182
|
+
slug,
|
|
183
|
+
authorId: session.user.id,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
revalidatePath('/posts');
|
|
188
|
+
return post;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function deletePost(id: string) {
|
|
192
|
+
const session = await auth();
|
|
193
|
+
if (!session) throw new Error('Unauthorized');
|
|
194
|
+
|
|
195
|
+
const post = await prisma.post.findUnique({
|
|
196
|
+
where: { id },
|
|
197
|
+
select: { authorId: true },
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (post?.authorId !== session.user.id) {
|
|
201
|
+
throw new Error('Forbidden');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await prisma.post.delete({ where: { id } });
|
|
205
|
+
revalidatePath('/posts');
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Transactions
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
export async function transferCredits(
|
|
213
|
+
fromUserId: string,
|
|
214
|
+
toUserId: string,
|
|
215
|
+
amount: number
|
|
216
|
+
) {
|
|
217
|
+
return prisma.$transaction(async (tx) => {
|
|
218
|
+
const sender = await tx.user.update({
|
|
219
|
+
where: { id: fromUserId },
|
|
220
|
+
data: { credits: { decrement: amount } },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (sender.credits < 0) {
|
|
224
|
+
throw new Error('Insufficient credits');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const recipient = await tx.user.update({
|
|
228
|
+
where: { id: toUserId },
|
|
229
|
+
data: { credits: { increment: amount } },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await tx.transaction.create({
|
|
233
|
+
data: {
|
|
234
|
+
fromUserId,
|
|
235
|
+
toUserId,
|
|
236
|
+
amount,
|
|
237
|
+
type: 'TRANSFER',
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return { sender, recipient };
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Migrations
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Create migration
|
|
250
|
+
npx prisma migrate dev --name add_user_role
|
|
251
|
+
|
|
252
|
+
# Apply migrations (production)
|
|
253
|
+
npx prisma migrate deploy
|
|
254
|
+
|
|
255
|
+
# Reset database (development)
|
|
256
|
+
npx prisma migrate reset
|
|
257
|
+
|
|
258
|
+
# Generate client
|
|
259
|
+
npx prisma generate
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Seeding
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// prisma/seed.ts
|
|
266
|
+
import { PrismaClient } from '@prisma/client';
|
|
267
|
+
|
|
268
|
+
const prisma = new PrismaClient();
|
|
269
|
+
|
|
270
|
+
async function main() {
|
|
271
|
+
// Clean up
|
|
272
|
+
await prisma.post.deleteMany();
|
|
273
|
+
await prisma.user.deleteMany();
|
|
274
|
+
|
|
275
|
+
// Create users
|
|
276
|
+
const alice = await prisma.user.create({
|
|
277
|
+
data: {
|
|
278
|
+
email: 'alice@example.com',
|
|
279
|
+
name: 'Alice',
|
|
280
|
+
role: 'ADMIN',
|
|
281
|
+
posts: {
|
|
282
|
+
create: [
|
|
283
|
+
{ title: 'First Post', slug: 'first-post', published: true },
|
|
284
|
+
{ title: 'Second Post', slug: 'second-post' },
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
console.log({ alice });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
main()
|
|
294
|
+
.catch(console.error)
|
|
295
|
+
.finally(() => prisma.$disconnect());
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
// package.json
|
|
300
|
+
{
|
|
301
|
+
"prisma": {
|
|
302
|
+
"seed": "tsx prisma/seed.ts"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Soft Deletes
|
|
308
|
+
|
|
309
|
+
```prisma
|
|
310
|
+
model User {
|
|
311
|
+
id String @id @default(cuid())
|
|
312
|
+
email String @unique
|
|
313
|
+
deletedAt DateTime?
|
|
314
|
+
|
|
315
|
+
@@index([deletedAt])
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Middleware for soft deletes
|
|
321
|
+
prisma.$use(async (params, next) => {
|
|
322
|
+
if (params.model === 'User') {
|
|
323
|
+
if (params.action === 'delete') {
|
|
324
|
+
params.action = 'update';
|
|
325
|
+
params.args['data'] = { deletedAt: new Date() };
|
|
326
|
+
}
|
|
327
|
+
if (params.action === 'findMany' || params.action === 'findFirst') {
|
|
328
|
+
params.args['where'] = {
|
|
329
|
+
...params.args['where'],
|
|
330
|
+
deletedAt: null,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return next(params);
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Optimization
|
|
339
|
+
|
|
340
|
+
### Select Only Needed Fields
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// BAD: Fetches all fields
|
|
344
|
+
const users = await prisma.user.findMany();
|
|
345
|
+
|
|
346
|
+
// GOOD: Select only needed
|
|
347
|
+
const users = await prisma.user.findMany({
|
|
348
|
+
select: { id: true, name: true, email: true },
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Avoid N+1
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// BAD: N+1 query
|
|
356
|
+
const posts = await prisma.post.findMany();
|
|
357
|
+
for (const post of posts) {
|
|
358
|
+
const author = await prisma.user.findUnique({ where: { id: post.authorId } });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// GOOD: Include relation
|
|
362
|
+
const posts = await prisma.post.findMany({
|
|
363
|
+
include: { author: { select: { name: true } } },
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Connection Pooling (Serverless)
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// For serverless (Vercel, AWS Lambda)
|
|
371
|
+
datasource db {
|
|
372
|
+
provider = "postgresql"
|
|
373
|
+
url = env("DATABASE_URL")
|
|
374
|
+
directUrl = env("DIRECT_URL") // For migrations
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Use connection pooler (PgBouncer, Prisma Accelerate)
|
|
378
|
+
// DATABASE_URL="prisma://accelerate.prisma-data.net/?api_key=..."
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Anti-patterns
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// BAD: Creating client in component
|
|
385
|
+
export default async function Page() {
|
|
386
|
+
const prisma = new PrismaClient(); // New connection each request!
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// GOOD: Use singleton
|
|
390
|
+
import { prisma } from '@/lib/prisma';
|
|
391
|
+
|
|
392
|
+
// BAD: Not handling errors
|
|
393
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
394
|
+
return user.name; // Might be null!
|
|
395
|
+
|
|
396
|
+
// GOOD: Handle null
|
|
397
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
398
|
+
if (!user) notFound();
|
|
399
|
+
return user.name;
|
|
400
|
+
```
|