@devmunna/agent-skillkit 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/ai-skills.js +5 -0
- package/dist/cli/commands/add.d.ts +2 -0
- package/dist/cli/commands/add.d.ts.map +1 -0
- package/dist/cli/commands/add.js +66 -0
- package/dist/cli/commands/add.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +33 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +145 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/list.d.ts +5 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +55 -0
- package/dist/cli/commands/list.js.map +1 -0
- package/dist/cli/commands/update.d.ts +2 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +49 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +22 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts/agent-selector.d.ts +3 -0
- package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
- package/dist/cli/prompts/agent-selector.js +23 -0
- package/dist/cli/prompts/agent-selector.js.map +1 -0
- package/dist/cli/prompts/stack-selector.d.ts +3 -0
- package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
- package/dist/cli/prompts/stack-selector.js +60 -0
- package/dist/cli/prompts/stack-selector.js.map +1 -0
- package/dist/core/config-manager.d.ts +20 -0
- package/dist/core/config-manager.d.ts.map +1 -0
- package/dist/core/config-manager.js +107 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/detector.d.ts +3 -0
- package/dist/core/detector.d.ts.map +1 -0
- package/dist/core/detector.js +50 -0
- package/dist/core/detector.js.map +1 -0
- package/dist/core/doctor.d.ts +12 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +102 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/skill-registry.d.ts +11 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +174 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-resolver.d.ts +3 -0
- package/dist/core/skill-resolver.d.ts.map +1 -0
- package/dist/core/skill-resolver.js +36 -0
- package/dist/core/skill-resolver.js.map +1 -0
- package/dist/core/validator.d.ts +13 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +99 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/generators/agent-installer.d.ts +5 -0
- package/dist/generators/agent-installer.d.ts.map +1 -0
- package/dist/generators/agent-installer.js +20 -0
- package/dist/generators/agent-installer.js.map +1 -0
- package/dist/generators/agents-md.d.ts +3 -0
- package/dist/generators/agents-md.d.ts.map +1 -0
- package/dist/generators/agents-md.js +70 -0
- package/dist/generators/agents-md.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +47 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/skill-generator.d.ts +5 -0
- package/dist/generators/skill-generator.d.ts.map +1 -0
- package/dist/generators/skill-generator.js +34 -0
- package/dist/generators/skill-generator.js.map +1 -0
- package/dist/generators/workflows.d.ts +3 -0
- package/dist/generators/workflows.d.ts.map +1 -0
- package/dist/generators/workflows.js +57 -0
- package/dist/generators/workflows.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +55 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file-utils.d.ts +12 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +39 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +73 -0
- package/skills/clean-architecture/SKILL.md +324 -0
- package/skills/express-mvc-prisma/SKILL.md +168 -0
- package/skills/express-mvc-prisma/references/auth.md +190 -0
- package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
- package/skills/express-mvc-prisma/references/error-handling.md +121 -0
- package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
- package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
- package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
- package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
- package/skills/fastify-rest/SKILL.md +287 -0
- package/skills/mongoose-odm/SKILL.md +281 -0
- package/skills/nextjs-fullstack/SKILL.md +328 -0
- package/skills/nextjs-fullstack/references/auth.md +270 -0
- package/skills/nextjs-fullstack/references/caching.md +157 -0
- package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
- package/skills/nextjs-fullstack/references/server-actions.md +214 -0
- package/skills/nextjs-fullstack/references/server-components.md +190 -0
- package/skills/node-base/SKILL.md +139 -0
- package/skills/prisma-orm/SKILL.md +334 -0
- package/skills/react-feature-arch/SKILL.md +208 -0
- package/skills/react-feature-arch/references/api-layer.md +110 -0
- package/skills/react-feature-arch/references/components.md +192 -0
- package/skills/react-feature-arch/references/data-fetching.md +198 -0
- package/skills/react-feature-arch/references/forms.md +194 -0
- package/skills/react-feature-arch/references/routing.md +148 -0
- package/skills/react-feature-arch/references/state-management.md +107 -0
- package/skills/tailwind-css/SKILL.md +236 -0
- package/skills/tailwind-css/references/components.md +340 -0
- package/skills/tailwind-css/references/design-tokens.md +230 -0
- package/skills/tailwind-css/references/patterns.md +375 -0
- package/skills/tailwind-css/references/setup.md +165 -0
- package/skills/zod-validation/SKILL.md +267 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Server Components Reference
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Decision: Server vs Client Component
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Does the component need:
|
|
9
|
+
useState / useEffect / useRef / useContext? → 'use client'
|
|
10
|
+
onClick / onChange / onSubmit? → 'use client'
|
|
11
|
+
window / localStorage / document? → 'use client'
|
|
12
|
+
useActionState / useOptimistic / useFormStatus? → 'use client'
|
|
13
|
+
Third-party client-only library? → 'use client'
|
|
14
|
+
─────────────────────────────────────────────
|
|
15
|
+
None of the above? → Server Component (default)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Push 'use client' as far down the tree as possible.** A page can be a Server Component that renders one or two leaf Client Components for interactivity.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Data Fetching in Server Components
|
|
23
|
+
|
|
24
|
+
Fetch directly — no useEffect, no loading state, no API client:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// app/(dashboard)/users/page.tsx
|
|
28
|
+
import { db } from '@/lib/db';
|
|
29
|
+
import { auth } from '@/lib/auth';
|
|
30
|
+
import { notFound } from 'next/navigation';
|
|
31
|
+
import { UserList } from '@/features/users/components/UserList';
|
|
32
|
+
import { CreateButton } from '@/features/users/components/CreateButton';
|
|
33
|
+
|
|
34
|
+
export default async function UsersPage() {
|
|
35
|
+
const session = await auth();
|
|
36
|
+
if (!session) redirect('/auth/login');
|
|
37
|
+
|
|
38
|
+
const users = await db.user.findMany({
|
|
39
|
+
select: { id: true, name: true, email: true, role: true, createdAt: true },
|
|
40
|
+
orderBy: { createdAt: 'desc' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<div className="header">
|
|
46
|
+
<h1>Users</h1>
|
|
47
|
+
<CreateButton /> {/* Client Component */}
|
|
48
|
+
</div>
|
|
49
|
+
<UserList users={users} />
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Parallel Data Fetching
|
|
58
|
+
|
|
59
|
+
Use `Promise.all` for independent queries — runs them concurrently:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
export default async function DashboardPage() {
|
|
63
|
+
const [users, products, stats] = await Promise.all([
|
|
64
|
+
db.user.count(),
|
|
65
|
+
db.product.count(),
|
|
66
|
+
getStats(),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
return <Dashboard users={users} products={products} stats={stats} />;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Dynamic Route — params as Promise (Next.js 15)
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// app/(dashboard)/users/[id]/page.tsx
|
|
79
|
+
import { notFound } from 'next/navigation';
|
|
80
|
+
import { db } from '@/lib/db';
|
|
81
|
+
|
|
82
|
+
interface Props {
|
|
83
|
+
params: Promise<{ id: string }>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default async function UserDetailPage({ params }: Props) {
|
|
87
|
+
const { id } = await params;
|
|
88
|
+
|
|
89
|
+
const user = await db.user.findUnique({
|
|
90
|
+
where: { id },
|
|
91
|
+
select: { id: true, name: true, email: true, role: true, createdAt: true },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!user) notFound();
|
|
95
|
+
|
|
96
|
+
return <UserDetail user={user} />;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Streaming with Suspense
|
|
103
|
+
|
|
104
|
+
Wrap slow data sources in `<Suspense>` to stream content progressively:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// app/(dashboard)/users/page.tsx
|
|
108
|
+
import { Suspense } from 'react';
|
|
109
|
+
|
|
110
|
+
export default function UsersPage() {
|
|
111
|
+
return (
|
|
112
|
+
<div>
|
|
113
|
+
<h1>Users</h1>
|
|
114
|
+
{/* Header renders immediately; list streams in */}
|
|
115
|
+
<Suspense fallback={<UserListSkeleton />}>
|
|
116
|
+
<UserListAsync />
|
|
117
|
+
</Suspense>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Separate async component wraps the slow fetch
|
|
123
|
+
async function UserListAsync() {
|
|
124
|
+
const users = await db.user.findMany(); // waits here, not in the parent
|
|
125
|
+
return <UserList users={users} />;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Passing Server Data to Client Components
|
|
132
|
+
|
|
133
|
+
Server Components can pass serializable props to Client Components:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// Server Component (parent)
|
|
137
|
+
export default async function UsersPage() {
|
|
138
|
+
const users = await db.user.findMany();
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<UserDataTable
|
|
142
|
+
users={users} // serialized and sent to client
|
|
143
|
+
initialPage={1}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 'use client' (child) — receives pre-fetched data
|
|
149
|
+
'use client';
|
|
150
|
+
export function UserDataTable({ users, initialPage }: Props) {
|
|
151
|
+
const [page, setPage] = useState(initialPage);
|
|
152
|
+
// uses users from server + client-side pagination
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Cannot pass:** functions, class instances, non-serializable objects.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Server Component Composition Patterns
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
// Composition: Server wraps Client
|
|
164
|
+
// ✅ Works — Server Component passes RSC children to Client
|
|
165
|
+
export default async function Layout({ children }: { children: React.ReactNode }) {
|
|
166
|
+
const session = await auth();
|
|
167
|
+
return (
|
|
168
|
+
<ClientSidebar session={session}>
|
|
169
|
+
{children}
|
|
170
|
+
</ClientSidebar>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ✅ Works — Server Component imported in another Server Component
|
|
175
|
+
import { ServerNav } from '@/components/server/ServerNav';
|
|
176
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
177
|
+
return (
|
|
178
|
+
<html>
|
|
179
|
+
<body>
|
|
180
|
+
<ServerNav />
|
|
181
|
+
{children}
|
|
182
|
+
</body>
|
|
183
|
+
</html>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ❌ Cannot — import Server Component inside 'use client' component
|
|
188
|
+
'use client';
|
|
189
|
+
import { ServerComponent } from './ServerComponent'; // error: server-only
|
|
190
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: node-base
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Base Node.js development standards. Apply this skill to every Node.js project regardless of framework. Covers async patterns, error handling foundations, environment configuration, file/folder conventions, logging, security defaults, and code quality rules.
|
|
6
|
+
stack: [node]
|
|
7
|
+
depends: []
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Node.js Base Skill
|
|
11
|
+
|
|
12
|
+
Baseline standards for every Node.js project. Framework-agnostic. Superseded by more specific skills (express-mvc-prisma, fastify-rest) when those are active.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Project Conventions
|
|
17
|
+
|
|
18
|
+
### Module system
|
|
19
|
+
- Use ES Modules (`"type": "module"` in package.json) for new projects
|
|
20
|
+
- Use `.js` extensions in all import paths
|
|
21
|
+
- Never mix `require()` and `import` in the same codebase
|
|
22
|
+
|
|
23
|
+
### File naming
|
|
24
|
+
- `kebab-case` for all files and directories
|
|
25
|
+
- No spaces, no underscores in file names
|
|
26
|
+
- Test files: `*.test.js` or `*.spec.js` adjacent to source
|
|
27
|
+
|
|
28
|
+
### Folder structure (generic)
|
|
29
|
+
```
|
|
30
|
+
src/
|
|
31
|
+
├── config/ # Env, DB, third-party clients
|
|
32
|
+
├── modules/ # Feature-grouped domain logic
|
|
33
|
+
├── middlewares/ # Cross-cutting concerns
|
|
34
|
+
├── utils/ # Pure utilities (no side effects)
|
|
35
|
+
└── types/ # Shared type definitions (TS projects)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Environment Variables
|
|
41
|
+
|
|
42
|
+
Always validate env at startup. Never read `process.env` outside the config module.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// src/config/env.js — minimal template
|
|
46
|
+
import { z } from 'zod';
|
|
47
|
+
import 'dotenv/config';
|
|
48
|
+
|
|
49
|
+
const schema = z.object({
|
|
50
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
51
|
+
PORT: z.coerce.number().default(3000),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = schema.safeParse(process.env);
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
console.error('❌ Missing env vars:', result.error.flatten().fieldErrors);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const env = result.data;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Async Patterns
|
|
66
|
+
|
|
67
|
+
- Always `async/await` — no `.then()/.catch()` chains
|
|
68
|
+
- Never swallow errors with empty `catch` blocks
|
|
69
|
+
- Top-level `await` is supported in ES Modules
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
// Good
|
|
73
|
+
const data = await fetchData();
|
|
74
|
+
|
|
75
|
+
// Bad
|
|
76
|
+
fetchData().then(data => { ... });
|
|
77
|
+
|
|
78
|
+
// Never
|
|
79
|
+
try { ... } catch (e) {} // swallowed error
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Error Handling
|
|
85
|
+
|
|
86
|
+
- Create an `AppError` class for operational errors (expected, safe to expose to client)
|
|
87
|
+
- Unhandled rejections and exceptions must crash the process in production
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// src/utils/AppError.js
|
|
91
|
+
export class AppError extends Error {
|
|
92
|
+
constructor(message, statusCode = 500) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.statusCode = statusCode;
|
|
95
|
+
this.isOperational = true;
|
|
96
|
+
Error.captureStackTrace(this, this.constructor);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Crash on unhandled rejections (in server.js / entry point)
|
|
101
|
+
process.on('unhandledRejection', (reason) => {
|
|
102
|
+
console.error('Unhandled Rejection:', reason);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Logging
|
|
110
|
+
|
|
111
|
+
- Use `console.error()` for errors (goes to stderr)
|
|
112
|
+
- Use `console.log()` for info (goes to stdout)
|
|
113
|
+
- Include context: `[module] message` format
|
|
114
|
+
- Never log passwords, tokens, or PII
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
console.error('[auth] Login failed:', email); // OK — no password
|
|
118
|
+
console.log('[server] Listening on port', port);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Security Defaults
|
|
124
|
+
|
|
125
|
+
Always apply these in HTTP servers:
|
|
126
|
+
- Disable `X-Powered-By` header: `app.disable('x-powered-by')`
|
|
127
|
+
- Validate and sanitize all user input before using it
|
|
128
|
+
- Never use `eval()`, `new Function()`, or `vm.runInThisContext()` with user input
|
|
129
|
+
- Use `crypto.randomBytes()` or `crypto.randomUUID()` for tokens — never `Math.random()`
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Code Quality Rules
|
|
134
|
+
|
|
135
|
+
- Max function length: ~30 lines. Extract if longer
|
|
136
|
+
- One export concept per file
|
|
137
|
+
- Pure utility functions must be side-effect-free
|
|
138
|
+
- No `var` — always `const`, use `let` only when reassignment is required
|
|
139
|
+
- No `any` in TypeScript — use `unknown` and narrow
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: prisma-orm
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: >
|
|
5
|
+
Apply this skill for any task involving Prisma ORM: designing schemas, writing migrations, querying data, handling relations, transactions, optimizing queries, or seeding. Triggers: "add Prisma to project", "design schema", "write a query", "handle Prisma errors", "transaction", "migrate database", "Prisma relations", "seeding", "Prisma v5".
|
|
6
|
+
stack: [prisma, postgresql]
|
|
7
|
+
depends: []
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Prisma ORM v5 — Production Skill
|
|
11
|
+
|
|
12
|
+
**Version target:** Prisma v5 · PostgreSQL · Node.js 20+ (ESM)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @prisma/client
|
|
20
|
+
npm install -D prisma
|
|
21
|
+
npx prisma init --datasource-provider postgresql
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Schema Conventions
|
|
27
|
+
|
|
28
|
+
```prisma
|
|
29
|
+
generator client {
|
|
30
|
+
provider = "prisma-client-js"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
datasource db {
|
|
34
|
+
provider = "postgresql"
|
|
35
|
+
url = env("DATABASE_URL")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
model User {
|
|
39
|
+
id String @id @default(uuid()) // UUID always, never autoincrement
|
|
40
|
+
email String @unique
|
|
41
|
+
name String
|
|
42
|
+
password String
|
|
43
|
+
role Role @default(USER) // enum for fixed value sets
|
|
44
|
+
posts Post[]
|
|
45
|
+
createdAt DateTime @default(now()) // always add timestamps
|
|
46
|
+
updatedAt DateTime @updatedAt
|
|
47
|
+
|
|
48
|
+
@@map("users") // snake_case table name always
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
model Post {
|
|
52
|
+
id String @id @default(uuid())
|
|
53
|
+
title String
|
|
54
|
+
body String
|
|
55
|
+
published Boolean @default(false)
|
|
56
|
+
authorId String
|
|
57
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
58
|
+
createdAt DateTime @default(now())
|
|
59
|
+
updatedAt DateTime @updatedAt
|
|
60
|
+
|
|
61
|
+
@@index([authorId]) // index every foreign key
|
|
62
|
+
@@map("posts")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
enum Role {
|
|
66
|
+
USER
|
|
67
|
+
ADMIN
|
|
68
|
+
MANAGER
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Schema rules (non-negotiable):**
|
|
73
|
+
- `@id @default(uuid())` on every model — never `@default(autoincrement())`
|
|
74
|
+
- `createdAt` + `updatedAt` on every model
|
|
75
|
+
- `@@map("snake_case")` on every model
|
|
76
|
+
- `@@index([fk])` on every foreign key field
|
|
77
|
+
- Enums for any column with a fixed set of values
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Prisma Singleton
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
// src/config/db.js (Express) or src/lib/db.ts (Next.js)
|
|
85
|
+
import { PrismaClient } from '@prisma/client';
|
|
86
|
+
import { isDev } from './env.js';
|
|
87
|
+
|
|
88
|
+
const globalForPrisma = globalThis;
|
|
89
|
+
|
|
90
|
+
export const prisma =
|
|
91
|
+
globalForPrisma.prisma ??
|
|
92
|
+
new PrismaClient({ log: isDev ? ['query', 'warn', 'error'] : ['error'] });
|
|
93
|
+
|
|
94
|
+
if (isDev) globalForPrisma.prisma = prisma;
|
|
95
|
+
|
|
96
|
+
export default prisma;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Service-Layer Queries (4-file pattern — no repository)
|
|
102
|
+
|
|
103
|
+
Services call Prisma directly. No separate repository file.
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
// modules/post/post.service.js
|
|
107
|
+
import prisma from '../../config/db.js';
|
|
108
|
+
import { Prisma } from '@prisma/client';
|
|
109
|
+
import { AppError } from '../../utils/AppError.js';
|
|
110
|
+
|
|
111
|
+
export const postService = {
|
|
112
|
+
async getAll({ page = 1, limit = 10, authorId } = {}) {
|
|
113
|
+
const skip = (page - 1) * limit;
|
|
114
|
+
const where = authorId ? { authorId } : {};
|
|
115
|
+
|
|
116
|
+
const [data, total] = await prisma.$transaction([
|
|
117
|
+
prisma.post.findMany({
|
|
118
|
+
where,
|
|
119
|
+
skip,
|
|
120
|
+
take: limit,
|
|
121
|
+
orderBy: { createdAt: 'desc' },
|
|
122
|
+
select: {
|
|
123
|
+
id: true, title: true, published: true, createdAt: true,
|
|
124
|
+
author: { select: { id: true, name: true } },
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
prisma.post.count({ where }),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
return { data, total, page, limit };
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async getById(id) {
|
|
134
|
+
const post = await prisma.post.findUnique({
|
|
135
|
+
where: { id },
|
|
136
|
+
include: { author: { select: { id: true, name: true } } },
|
|
137
|
+
});
|
|
138
|
+
if (!post) throw new AppError('Post not found', 404);
|
|
139
|
+
return post;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async create(data) {
|
|
143
|
+
try {
|
|
144
|
+
return await prisma.post.create({ data });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
147
|
+
if (err.code === 'P2002') throw new AppError('Duplicate entry', 409);
|
|
148
|
+
if (err.code === 'P2003') throw new AppError('Related record not found', 400);
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
async update(id, data) {
|
|
155
|
+
try {
|
|
156
|
+
return await prisma.post.update({ where: { id }, data });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
|
159
|
+
if (err.code === 'P2025') throw new AppError('Post not found', 404);
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async remove(id) {
|
|
166
|
+
await prisma.post.delete({ where: { id } });
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Prisma Error Codes — Handle in Service Layer
|
|
174
|
+
|
|
175
|
+
| Code | Meaning | Response |
|
|
176
|
+
|------|---------|--------|
|
|
177
|
+
| P2002 | Unique constraint violation | 409 Conflict |
|
|
178
|
+
| P2025 | Record not found (update/delete) | 404 Not Found |
|
|
179
|
+
| P2003 | Foreign key constraint failed | 400 Bad Request |
|
|
180
|
+
| P2014 | Relation violation | 400 Bad Request |
|
|
181
|
+
|
|
182
|
+
The global `errorHandler` also catches raw `PrismaClientKnownRequestError` as a fallback.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Transactions
|
|
187
|
+
|
|
188
|
+
Use `$transaction` for multi-step writes that must be atomic:
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
// Batch (parallel) — read count + list
|
|
192
|
+
const [data, total] = await prisma.$transaction([
|
|
193
|
+
prisma.post.findMany({ skip, take }),
|
|
194
|
+
prisma.post.count(),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
// Interactive (sequential) — reads depend on previous writes
|
|
198
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
199
|
+
const order = await tx.order.create({ data: orderData });
|
|
200
|
+
|
|
201
|
+
await tx.product.update({
|
|
202
|
+
where: { id: orderData.productId },
|
|
203
|
+
data: { stock: { decrement: orderData.quantity } },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await tx.payment.create({
|
|
207
|
+
data: { orderId: order.id, amount: orderData.total },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return order;
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Query Optimization
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
// Select only needed fields — never return password or secrets
|
|
220
|
+
prisma.user.findMany({
|
|
221
|
+
select: { id: true, name: true, email: true, role: true },
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Include relations with select — avoid N+1 and fetching unnecessary data
|
|
225
|
+
prisma.post.findMany({
|
|
226
|
+
include: {
|
|
227
|
+
author: { select: { id: true, name: true } },
|
|
228
|
+
_count: { select: { comments: true } }, // count without loading records
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Cursor-based pagination for large datasets
|
|
233
|
+
prisma.post.findMany({
|
|
234
|
+
take: 10,
|
|
235
|
+
skip: 1, // skip the cursor itself
|
|
236
|
+
cursor: { id: lastId },
|
|
237
|
+
orderBy: { createdAt: 'asc' },
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Relation Patterns
|
|
244
|
+
|
|
245
|
+
```prisma
|
|
246
|
+
// One-to-many
|
|
247
|
+
model Category {
|
|
248
|
+
id String @id @default(uuid())
|
|
249
|
+
name String
|
|
250
|
+
posts Post[]
|
|
251
|
+
|
|
252
|
+
@@map("categories")
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
model Post {
|
|
256
|
+
categoryId String?
|
|
257
|
+
category Category? @relation(fields: [categoryId], references: [id])
|
|
258
|
+
|
|
259
|
+
@@index([categoryId])
|
|
260
|
+
@@map("posts")
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Many-to-many (explicit join table)
|
|
264
|
+
model Post {
|
|
265
|
+
tags PostTag[]
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
model Tag {
|
|
269
|
+
posts PostTag[]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
model PostTag {
|
|
273
|
+
postId String
|
|
274
|
+
tagId String
|
|
275
|
+
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
276
|
+
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
277
|
+
|
|
278
|
+
@@id([postId, tagId])
|
|
279
|
+
@@map("post_tags")
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Migration Commands
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
npx prisma migrate dev --name <description> # create + apply (dev only)
|
|
289
|
+
npx prisma migrate deploy # apply pending (production)
|
|
290
|
+
npx prisma migrate reset # reset + reseed (dev only)
|
|
291
|
+
npx prisma generate # regenerate Prisma Client
|
|
292
|
+
npx prisma studio # GUI browser
|
|
293
|
+
npx prisma db seed # run seed file
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Seeding
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// prisma/seed.js
|
|
302
|
+
import { PrismaClient } from '@prisma/client';
|
|
303
|
+
import bcrypt from 'bcrypt';
|
|
304
|
+
|
|
305
|
+
const prisma = new PrismaClient();
|
|
306
|
+
|
|
307
|
+
async function main() {
|
|
308
|
+
const passwordHash = await bcrypt.hash('password123', 12);
|
|
309
|
+
|
|
310
|
+
await prisma.user.upsert({
|
|
311
|
+
where: { email: 'admin@example.com' },
|
|
312
|
+
update: {},
|
|
313
|
+
create: {
|
|
314
|
+
email: 'admin@example.com',
|
|
315
|
+
name: 'Admin',
|
|
316
|
+
password: passwordHash,
|
|
317
|
+
role: 'ADMIN',
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
console.log('Seeding complete');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
main()
|
|
325
|
+
.catch(console.error)
|
|
326
|
+
.finally(() => prisma.$disconnect());
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
In `package.json`:
|
|
330
|
+
```json
|
|
331
|
+
{
|
|
332
|
+
"prisma": { "seed": "node prisma/seed.js" }
|
|
333
|
+
}
|
|
334
|
+
```
|