red64-cli 0.1.0 → 0.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 +1 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/ListScreen.d.ts.map +1 -1
- package/dist/components/screens/ListScreen.js +28 -3
- package/dist/components/screens/ListScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +212 -13
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
- package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
- package/dist/components/ui/ArtifactsSidebar.js +51 -0
- package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
- package/dist/components/ui/FeatureSidebar.js +1 -1
- package/dist/components/ui/FeatureSidebar.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +3 -3
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Prisma Migrations
|
|
2
|
+
|
|
3
|
+
Database migration workflow for Prisma ORM with zero-downtime strategies, seed data, and production deployment.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Schema-first**: Change `schema.prisma`, then generate the migration
|
|
10
|
+
- **Version-controlled**: Every migration is committed and never modified after deployment
|
|
11
|
+
- **Reversible in practice**: Plan rollback strategies even though Prisma does not generate down migrations
|
|
12
|
+
- **Zero-downtime by default**: Structure changes to avoid locking tables or breaking running code
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Migration Workflow
|
|
17
|
+
|
|
18
|
+
### Development Cycle
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Edit schema.prisma
|
|
22
|
+
# 2. Generate and apply migration
|
|
23
|
+
pnpm prisma migrate dev --name add_user_bio
|
|
24
|
+
|
|
25
|
+
# 3. Prisma automatically:
|
|
26
|
+
# - Generates SQL migration file
|
|
27
|
+
# - Applies it to development database
|
|
28
|
+
# - Regenerates Prisma Client
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Migration Commands
|
|
32
|
+
|
|
33
|
+
| Command | Purpose | Environment |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `prisma migrate dev` | Create and apply migration | Development |
|
|
36
|
+
| `prisma migrate dev --name <name>` | Named migration | Development |
|
|
37
|
+
| `prisma migrate deploy` | Apply pending migrations | Production/CI |
|
|
38
|
+
| `prisma migrate reset` | Drop database, re-apply all migrations + seed | Development |
|
|
39
|
+
| `prisma migrate status` | Show pending migrations | Any |
|
|
40
|
+
| `prisma db push` | Push schema without migration file | Prototyping only |
|
|
41
|
+
|
|
42
|
+
### migrate dev vs db push
|
|
43
|
+
|
|
44
|
+
| Feature | `migrate dev` | `db push` |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| Creates migration file | Yes | No |
|
|
47
|
+
| Version-controlled | Yes | No |
|
|
48
|
+
| Safe for production | Yes | No |
|
|
49
|
+
| Handles data loss warnings | Yes | May silently drop data |
|
|
50
|
+
| Use case | All real development | Quick prototyping, throwaway databases |
|
|
51
|
+
|
|
52
|
+
**Rule**: Always use `migrate dev` once your schema is past prototyping phase. Never use `db push` in production.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Migration File Structure
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
prisma/
|
|
60
|
+
migrations/
|
|
61
|
+
20240115103000_init/
|
|
62
|
+
migration.sql
|
|
63
|
+
20240116140000_add_user_bio/
|
|
64
|
+
migration.sql
|
|
65
|
+
20240118090000_add_post_status/
|
|
66
|
+
migration.sql
|
|
67
|
+
migration_lock.toml # Locks provider (postgresql)
|
|
68
|
+
schema.prisma
|
|
69
|
+
seed.ts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Generated SQL Example
|
|
73
|
+
|
|
74
|
+
```sql
|
|
75
|
+
-- prisma/migrations/20240116140000_add_user_bio/migration.sql
|
|
76
|
+
-- AlterTable
|
|
77
|
+
ALTER TABLE "users" ADD COLUMN "bio" TEXT;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Naming Convention
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Format: descriptive verb + subject
|
|
84
|
+
pnpm prisma migrate dev --name init
|
|
85
|
+
pnpm prisma migrate dev --name add_user_bio
|
|
86
|
+
pnpm prisma migrate dev --name add_post_status_index
|
|
87
|
+
pnpm prisma migrate dev --name create_comments_table
|
|
88
|
+
pnpm prisma migrate dev --name make_email_unique
|
|
89
|
+
pnpm prisma migrate dev --name remove_legacy_fields
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Zero-Downtime Migration Patterns
|
|
95
|
+
|
|
96
|
+
### Adding a Column
|
|
97
|
+
|
|
98
|
+
```prisma
|
|
99
|
+
// Safe: new nullable column with no default
|
|
100
|
+
model User {
|
|
101
|
+
// existing fields...
|
|
102
|
+
bio String? @map("bio") // New field - nullable, no data migration needed
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pnpm prisma migrate dev --name add_user_bio
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Adding a Required Column (Two-Step)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
# Step 1: Add as nullable, deploy code that writes to it
|
|
114
|
+
model User {
|
|
115
|
+
phoneNumber String? @map("phone_number")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Step 2: After backfilling, make required
|
|
119
|
+
model User {
|
|
120
|
+
phoneNumber String @map("phone_number")
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Backfill script (run between step 1 and step 2)
|
|
126
|
+
// scripts/backfill-phone.ts
|
|
127
|
+
import { prisma } from "../src/lib/prisma";
|
|
128
|
+
|
|
129
|
+
async function backfill() {
|
|
130
|
+
const batchSize = 1000;
|
|
131
|
+
let processed = 0;
|
|
132
|
+
|
|
133
|
+
while (true) {
|
|
134
|
+
const users = await prisma.user.findMany({
|
|
135
|
+
where: { phoneNumber: null },
|
|
136
|
+
take: batchSize,
|
|
137
|
+
select: { id: true },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (users.length === 0) break;
|
|
141
|
+
|
|
142
|
+
await prisma.user.updateMany({
|
|
143
|
+
where: { id: { in: users.map((u) => u.id) } },
|
|
144
|
+
data: { phoneNumber: "PENDING" },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
processed += users.length;
|
|
148
|
+
console.log(`Backfilled ${processed} users`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
backfill();
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Renaming a Column (Three-Step)
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
# Step 1: Add new column, deploy code that writes to both
|
|
159
|
+
# Step 2: Backfill new column, deploy code that reads from new column
|
|
160
|
+
# Step 3: Remove old column
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Rule**: Never rename a column in a single migration. The running application will break between deploy and restart.
|
|
164
|
+
|
|
165
|
+
### Adding an Index
|
|
166
|
+
|
|
167
|
+
```prisma
|
|
168
|
+
model Post {
|
|
169
|
+
// ...
|
|
170
|
+
@@index([authorId, status])
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
For large tables, consider creating the index concurrently by editing the generated SQL:
|
|
175
|
+
|
|
176
|
+
```sql
|
|
177
|
+
-- Edit the migration.sql before applying
|
|
178
|
+
CREATE INDEX CONCURRENTLY "Post_authorId_status_idx" ON "posts" ("author_id", "status");
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Note**: `CONCURRENTLY` only works in PostgreSQL and cannot run inside a transaction. Edit the migration SQL manually.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Seed Data
|
|
186
|
+
|
|
187
|
+
### Seed Script
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// prisma/seed.ts
|
|
191
|
+
import { PrismaClient, UserRole } from "@prisma/client";
|
|
192
|
+
import { hash } from "bcrypt";
|
|
193
|
+
|
|
194
|
+
const prisma = new PrismaClient();
|
|
195
|
+
|
|
196
|
+
async function main() {
|
|
197
|
+
// Idempotent: use upsert to avoid duplicates on re-run
|
|
198
|
+
const admin = await prisma.user.upsert({
|
|
199
|
+
where: { email: "admin@example.com" },
|
|
200
|
+
update: {},
|
|
201
|
+
create: {
|
|
202
|
+
email: "admin@example.com",
|
|
203
|
+
name: "Admin User",
|
|
204
|
+
hashedPassword: await hash("password123", 12),
|
|
205
|
+
role: UserRole.ADMIN,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
console.log(`Seeded admin user: ${admin.id}`);
|
|
210
|
+
|
|
211
|
+
// Seed sample data for development
|
|
212
|
+
if (process.env.NODE_ENV !== "production") {
|
|
213
|
+
const posts = await Promise.all(
|
|
214
|
+
Array.from({ length: 10 }).map((_, i) =>
|
|
215
|
+
prisma.post.upsert({
|
|
216
|
+
where: { id: `seed-post-${i}` },
|
|
217
|
+
update: {},
|
|
218
|
+
create: {
|
|
219
|
+
id: `seed-post-${i}`,
|
|
220
|
+
title: `Sample Post ${i + 1}`,
|
|
221
|
+
body: `Content for post ${i + 1}`,
|
|
222
|
+
authorId: admin.id,
|
|
223
|
+
status: i % 3 === 0 ? "PUBLISHED" : "DRAFT",
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
console.log(`Seeded ${posts.length} posts`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
main()
|
|
233
|
+
.catch((e) => {
|
|
234
|
+
console.error(e);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
})
|
|
237
|
+
.finally(() => prisma.$disconnect());
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Configure Seed Command
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
// package.json
|
|
244
|
+
{
|
|
245
|
+
"prisma": {
|
|
246
|
+
"seed": "tsx prisma/seed.ts"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Run seed
|
|
253
|
+
pnpm prisma db seed
|
|
254
|
+
|
|
255
|
+
# Reset + seed
|
|
256
|
+
pnpm prisma migrate reset
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Production Deployment
|
|
262
|
+
|
|
263
|
+
### CI/CD Pipeline
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
# .github/workflows/deploy.yml (relevant steps)
|
|
267
|
+
- name: Apply migrations
|
|
268
|
+
run: pnpm prisma migrate deploy
|
|
269
|
+
env:
|
|
270
|
+
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
|
271
|
+
|
|
272
|
+
- name: Deploy application
|
|
273
|
+
run: # deploy command
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Deployment Order
|
|
277
|
+
|
|
278
|
+
1. Run `prisma migrate deploy` (applies pending migrations)
|
|
279
|
+
2. Deploy new application code
|
|
280
|
+
3. Verify health check passes
|
|
281
|
+
|
|
282
|
+
**Rule**: Always run migrations before deploying new code. The old code must work with the new schema (see zero-downtime patterns above).
|
|
283
|
+
|
|
284
|
+
### Rollback Strategy
|
|
285
|
+
|
|
286
|
+
Prisma does not generate down migrations. Plan rollbacks manually:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# Option 1: Create a new "undo" migration
|
|
290
|
+
pnpm prisma migrate dev --name revert_add_bio
|
|
291
|
+
# Manually write the SQL to reverse the change
|
|
292
|
+
|
|
293
|
+
# Option 2: Restore from database backup (last resort)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Best practice**: Design migrations to be forward-only. If adding a column breaks things, the code should handle both states.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Testing Migrations
|
|
301
|
+
|
|
302
|
+
### Test Against Clean Database
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# Verify all migrations apply cleanly from scratch
|
|
306
|
+
pnpm prisma migrate reset --force
|
|
307
|
+
pnpm prisma migrate deploy
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### CI Migration Check
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# In CI, verify no pending migrations exist in development
|
|
314
|
+
pnpm prisma migrate diff --from-schema-datamodel prisma/schema.prisma --to-migrations prisma/migrations --exit-code
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Anti-Patterns
|
|
320
|
+
|
|
321
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| `db push` in production | No migration history, data loss risk | Always use `migrate deploy` |
|
|
324
|
+
| Editing deployed migrations | Breaks migration checksums | Create new migrations to fix issues |
|
|
325
|
+
| Adding NOT NULL without default | Breaks existing rows | Add nullable first, backfill, then constrain |
|
|
326
|
+
| Single-step column rename | Running code breaks immediately | Three-step: add, migrate, remove |
|
|
327
|
+
| No seed script | Manual data setup for every developer | Maintain `prisma/seed.ts` |
|
|
328
|
+
| Skipping CI migration check | Schema drift between environments | Check migration status in CI |
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
_Migrations are deployments. Treat them with the same care: test them, version them, and always have a rollback plan._
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Prisma Schema Design
|
|
2
|
+
|
|
3
|
+
Best practices for Prisma schema definition, naming conventions, relations, and type-safe validation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Schema is the source of truth**: Database structure defined in `schema.prisma`, validated by Zod
|
|
10
|
+
- **Database enforces integrity**: Constraints live in the schema, not just application code
|
|
11
|
+
- **Generated types everywhere**: Prisma Client types flow from schema to UI
|
|
12
|
+
- **Thin models, rich services**: Business logic belongs in service functions, not in the schema
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Schema Configuration
|
|
17
|
+
|
|
18
|
+
```prisma
|
|
19
|
+
// prisma/schema.prisma
|
|
20
|
+
generator client {
|
|
21
|
+
provider = "prisma-client-js"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
datasource db {
|
|
25
|
+
provider = "postgresql"
|
|
26
|
+
url = env("DATABASE_URL")
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Model Definition
|
|
33
|
+
|
|
34
|
+
### Complete Example
|
|
35
|
+
|
|
36
|
+
```prisma
|
|
37
|
+
model User {
|
|
38
|
+
id String @id @default(cuid())
|
|
39
|
+
email String @unique
|
|
40
|
+
name String
|
|
41
|
+
hashedPassword String @map("hashed_password")
|
|
42
|
+
bio String?
|
|
43
|
+
avatarUrl String? @map("avatar_url")
|
|
44
|
+
role UserRole @default(MEMBER)
|
|
45
|
+
isActive Boolean @default(true) @map("is_active")
|
|
46
|
+
lastLoginAt DateTime? @map("last_login_at")
|
|
47
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
48
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
49
|
+
|
|
50
|
+
// Relations
|
|
51
|
+
posts Post[]
|
|
52
|
+
accounts Account[]
|
|
53
|
+
sessions Session[]
|
|
54
|
+
|
|
55
|
+
@@map("users")
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Naming Conventions
|
|
60
|
+
|
|
61
|
+
| Entity | Convention | Example |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| Model name | PascalCase singular | `User`, `PostTag` |
|
|
64
|
+
| Field name | camelCase | `createdAt`, `hashedPassword` |
|
|
65
|
+
| Table name | snake_case plural via `@@map` | `@@map("users")` |
|
|
66
|
+
| Column name | snake_case via `@map` | `@map("hashed_password")` |
|
|
67
|
+
| Relation field | camelCase, matches related model | `posts`, `author` |
|
|
68
|
+
| Enum name | PascalCase | `UserRole`, `PostStatus` |
|
|
69
|
+
| Enum values | UPPER_SNAKE_CASE | `ADMIN`, `DRAFT` |
|
|
70
|
+
|
|
71
|
+
**Why `@map`**: Prisma field names stay camelCase for TypeScript ergonomics while database columns use snake_case (SQL convention).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Field Patterns
|
|
76
|
+
|
|
77
|
+
### IDs
|
|
78
|
+
|
|
79
|
+
```prisma
|
|
80
|
+
// CUID (default recommendation)
|
|
81
|
+
id String @id @default(cuid())
|
|
82
|
+
|
|
83
|
+
// UUID
|
|
84
|
+
id String @id @default(uuid())
|
|
85
|
+
|
|
86
|
+
// Auto-increment (avoid for distributed systems)
|
|
87
|
+
id Int @id @default(autoincrement())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Decision**: Use `cuid()` by default. Non-sequential, URL-safe, and collision-resistant.
|
|
91
|
+
|
|
92
|
+
### Timestamps
|
|
93
|
+
|
|
94
|
+
```prisma
|
|
95
|
+
// Every model gets these. No exceptions.
|
|
96
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
97
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Soft Deletes
|
|
101
|
+
|
|
102
|
+
```prisma
|
|
103
|
+
model Post {
|
|
104
|
+
// ... other fields
|
|
105
|
+
deletedAt DateTime? @map("deleted_at")
|
|
106
|
+
|
|
107
|
+
@@index([deletedAt])
|
|
108
|
+
@@map("posts")
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Querying with soft deletes
|
|
114
|
+
const activePosts = await prisma.post.findMany({
|
|
115
|
+
where: { deletedAt: null },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Soft delete operation
|
|
119
|
+
await prisma.post.update({
|
|
120
|
+
where: { id },
|
|
121
|
+
data: { deletedAt: new Date() },
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Optional vs Required
|
|
126
|
+
|
|
127
|
+
```prisma
|
|
128
|
+
// Required (non-null) - field must always have a value
|
|
129
|
+
name String
|
|
130
|
+
|
|
131
|
+
// Optional (nullable) - use ? suffix
|
|
132
|
+
bio String?
|
|
133
|
+
|
|
134
|
+
// Required with default - automatically set if not provided
|
|
135
|
+
role UserRole @default(MEMBER)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Rule**: Default to required. Only make fields optional when null has genuine meaning (e.g., "not yet set" vs "empty string").
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Enums
|
|
143
|
+
|
|
144
|
+
```prisma
|
|
145
|
+
enum UserRole {
|
|
146
|
+
ADMIN
|
|
147
|
+
MEMBER
|
|
148
|
+
VIEWER
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
enum PostStatus {
|
|
152
|
+
DRAFT
|
|
153
|
+
PUBLISHED
|
|
154
|
+
ARCHIVED
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
model Post {
|
|
158
|
+
status PostStatus @default(DRAFT)
|
|
159
|
+
// ...
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### When to Use Enums vs Strings
|
|
164
|
+
|
|
165
|
+
| Approach | When to Use |
|
|
166
|
+
|---|---|
|
|
167
|
+
| Prisma enum | Fixed set of values that rarely changes |
|
|
168
|
+
| String field | Values that change often, user-defined categories |
|
|
169
|
+
|
|
170
|
+
**Note**: Adding a value to a Prisma enum requires a migration. For frequently changing sets, use a string field with Zod validation.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Relations
|
|
175
|
+
|
|
176
|
+
### One-to-Many
|
|
177
|
+
|
|
178
|
+
```prisma
|
|
179
|
+
model User {
|
|
180
|
+
id String @id @default(cuid())
|
|
181
|
+
posts Post[]
|
|
182
|
+
|
|
183
|
+
@@map("users")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
model Post {
|
|
187
|
+
id String @id @default(cuid())
|
|
188
|
+
authorId String @map("author_id")
|
|
189
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
190
|
+
|
|
191
|
+
@@index([authorId])
|
|
192
|
+
@@map("posts")
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### One-to-One
|
|
197
|
+
|
|
198
|
+
```prisma
|
|
199
|
+
model User {
|
|
200
|
+
id String @id @default(cuid())
|
|
201
|
+
profile Profile?
|
|
202
|
+
|
|
203
|
+
@@map("users")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
model Profile {
|
|
207
|
+
id String @id @default(cuid())
|
|
208
|
+
userId String @unique @map("user_id")
|
|
209
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
210
|
+
bio String?
|
|
211
|
+
|
|
212
|
+
@@map("profiles")
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Many-to-Many (Explicit Join Table)
|
|
217
|
+
|
|
218
|
+
```prisma
|
|
219
|
+
model Post {
|
|
220
|
+
id String @id @default(cuid())
|
|
221
|
+
tags PostTag[]
|
|
222
|
+
|
|
223
|
+
@@map("posts")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
model Tag {
|
|
227
|
+
id String @id @default(cuid())
|
|
228
|
+
name String @unique
|
|
229
|
+
posts PostTag[]
|
|
230
|
+
|
|
231
|
+
@@map("tags")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
model PostTag {
|
|
235
|
+
postId String @map("post_id")
|
|
236
|
+
tagId String @map("tag_id")
|
|
237
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
238
|
+
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
239
|
+
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
240
|
+
|
|
241
|
+
@@id([postId, tagId])
|
|
242
|
+
@@map("post_tags")
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Rule**: Always use explicit join tables over Prisma's implicit many-to-many. Explicit tables support additional fields (timestamps, ordering) and are easier to query.
|
|
247
|
+
|
|
248
|
+
### Cascade Behaviors
|
|
249
|
+
|
|
250
|
+
| Behavior | When to Use |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `Cascade` | Child cannot exist without parent (comments on post) |
|
|
253
|
+
| `SetNull` | Child can exist independently (optional foreign key) |
|
|
254
|
+
| `Restrict` | Prevent deleting parent with existing children |
|
|
255
|
+
| `NoAction` | Database-level, similar to Restrict |
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Indexes
|
|
260
|
+
|
|
261
|
+
```prisma
|
|
262
|
+
model Post {
|
|
263
|
+
id String @id @default(cuid())
|
|
264
|
+
authorId String @map("author_id")
|
|
265
|
+
status PostStatus @default(DRAFT)
|
|
266
|
+
slug String
|
|
267
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
268
|
+
|
|
269
|
+
// Single-column index
|
|
270
|
+
@@index([authorId])
|
|
271
|
+
|
|
272
|
+
// Composite index (query pattern: "posts by author filtered by status")
|
|
273
|
+
@@index([authorId, status])
|
|
274
|
+
|
|
275
|
+
// Unique composite (slug unique per author)
|
|
276
|
+
@@unique([authorId, slug])
|
|
277
|
+
|
|
278
|
+
@@map("posts")
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Indexing Rules
|
|
283
|
+
|
|
284
|
+
- Always index foreign key columns
|
|
285
|
+
- Add composite indexes for frequent multi-column queries
|
|
286
|
+
- Use unique constraints for business uniqueness rules
|
|
287
|
+
- Do not over-index; each index slows writes
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## JSON Fields
|
|
292
|
+
|
|
293
|
+
```prisma
|
|
294
|
+
model User {
|
|
295
|
+
id String @id @default(cuid())
|
|
296
|
+
preferences Json @default("{}")
|
|
297
|
+
|
|
298
|
+
@@map("users")
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Type-safe access with Zod
|
|
304
|
+
import { z } from "zod";
|
|
305
|
+
|
|
306
|
+
const preferencesSchema = z.object({
|
|
307
|
+
theme: z.enum(["light", "dark"]).default("light"),
|
|
308
|
+
emailNotifications: z.boolean().default(true),
|
|
309
|
+
language: z.string().default("en"),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
type UserPreferences = z.infer<typeof preferencesSchema>;
|
|
313
|
+
|
|
314
|
+
function getPreferences(raw: unknown): UserPreferences {
|
|
315
|
+
return preferencesSchema.parse(raw);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Warning**: JSON fields bypass Prisma's type system. Always validate with Zod when reading.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Zod Validation Schemas
|
|
324
|
+
|
|
325
|
+
### Paired with Prisma Models
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// lib/validations/user.ts
|
|
329
|
+
import { z } from "zod";
|
|
330
|
+
|
|
331
|
+
export const createUserSchema = z.object({
|
|
332
|
+
email: z.string().email("Invalid email address"),
|
|
333
|
+
name: z.string().min(1, "Name is required").max(255),
|
|
334
|
+
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
export const updateUserSchema = z.object({
|
|
338
|
+
name: z.string().min(1).max(255).optional(),
|
|
339
|
+
bio: z.string().max(500).optional(),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
|
343
|
+
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Anti-Patterns
|
|
349
|
+
|
|
350
|
+
| Anti-Pattern | Problem | Correct Approach |
|
|
351
|
+
|---|---|---|
|
|
352
|
+
| Implicit many-to-many | Cannot add fields to join table later | Explicit join model with `@@id` |
|
|
353
|
+
| No `@map` / `@@map` | Inconsistent naming across TypeScript and SQL | Map camelCase fields to snake_case columns |
|
|
354
|
+
| Missing indexes on foreign keys | Slow joins and lookups | `@@index` on every foreign key |
|
|
355
|
+
| Business logic in schema | Prisma schema is declarative only | Put logic in service functions |
|
|
356
|
+
| `Int` IDs for distributed systems | Collisions across replicas | Use `cuid()` or `uuid()` |
|
|
357
|
+
| No timestamps on models | Cannot debug or audit data | Always add `createdAt` and `updatedAt` |
|
|
358
|
+
| Storing computed values | Gets out of sync | Compute at query time or use database views |
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
_The schema defines structure and integrity. Validation belongs in Zod. Business rules belong in services._
|