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.
Files changed (125) hide show
  1. package/README.md +1 -2
  2. package/dist/cli/parseArgs.d.ts.map +1 -1
  3. package/dist/cli/parseArgs.js +5 -0
  4. package/dist/cli/parseArgs.js.map +1 -1
  5. package/dist/components/init/CompleteStep.d.ts.map +1 -1
  6. package/dist/components/init/CompleteStep.js +2 -2
  7. package/dist/components/init/CompleteStep.js.map +1 -1
  8. package/dist/components/init/TestCheckStep.d.ts +16 -0
  9. package/dist/components/init/TestCheckStep.d.ts.map +1 -0
  10. package/dist/components/init/TestCheckStep.js +120 -0
  11. package/dist/components/init/TestCheckStep.js.map +1 -0
  12. package/dist/components/init/index.d.ts +1 -0
  13. package/dist/components/init/index.d.ts.map +1 -1
  14. package/dist/components/init/index.js +1 -0
  15. package/dist/components/init/index.js.map +1 -1
  16. package/dist/components/init/types.d.ts +9 -0
  17. package/dist/components/init/types.d.ts.map +1 -1
  18. package/dist/components/screens/InitScreen.d.ts.map +1 -1
  19. package/dist/components/screens/InitScreen.js +69 -6
  20. package/dist/components/screens/InitScreen.js.map +1 -1
  21. package/dist/components/screens/ListScreen.d.ts.map +1 -1
  22. package/dist/components/screens/ListScreen.js +28 -3
  23. package/dist/components/screens/ListScreen.js.map +1 -1
  24. package/dist/components/screens/StartScreen.d.ts.map +1 -1
  25. package/dist/components/screens/StartScreen.js +212 -13
  26. package/dist/components/screens/StartScreen.js.map +1 -1
  27. package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
  28. package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
  29. package/dist/components/ui/ArtifactsSidebar.js +51 -0
  30. package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
  31. package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
  32. package/dist/components/ui/FeatureSidebar.js +1 -1
  33. package/dist/components/ui/FeatureSidebar.js.map +1 -1
  34. package/dist/components/ui/index.d.ts +1 -0
  35. package/dist/components/ui/index.d.ts.map +1 -1
  36. package/dist/components/ui/index.js +1 -0
  37. package/dist/components/ui/index.js.map +1 -1
  38. package/dist/services/ClaudeErrorDetector.js +3 -3
  39. package/dist/services/ClaudeErrorDetector.js.map +1 -1
  40. package/dist/services/ConfigService.d.ts +1 -0
  41. package/dist/services/ConfigService.d.ts.map +1 -1
  42. package/dist/services/ConfigService.js.map +1 -1
  43. package/dist/services/ProjectDetector.d.ts +28 -0
  44. package/dist/services/ProjectDetector.d.ts.map +1 -0
  45. package/dist/services/ProjectDetector.js +236 -0
  46. package/dist/services/ProjectDetector.js.map +1 -0
  47. package/dist/services/TestRunner.d.ts +46 -0
  48. package/dist/services/TestRunner.d.ts.map +1 -0
  49. package/dist/services/TestRunner.js +85 -0
  50. package/dist/services/TestRunner.js.map +1 -0
  51. package/dist/services/index.d.ts +2 -0
  52. package/dist/services/index.d.ts.map +1 -1
  53. package/dist/services/index.js +2 -0
  54. package/dist/services/index.js.map +1 -1
  55. package/dist/types/index.d.ts +13 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/dist/types/index.js.map +1 -1
  58. package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
  59. package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
  60. package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
  61. package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
  62. package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
  63. package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
  64. package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
  65. package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
  66. package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
  67. package/framework/stacks/generic/feedback.md +80 -0
  68. package/framework/stacks/nextjs/accessibility.md +437 -0
  69. package/framework/stacks/nextjs/api.md +431 -0
  70. package/framework/stacks/nextjs/coding-style.md +282 -0
  71. package/framework/stacks/nextjs/commenting.md +226 -0
  72. package/framework/stacks/nextjs/components.md +411 -0
  73. package/framework/stacks/nextjs/conventions.md +333 -0
  74. package/framework/stacks/nextjs/css.md +310 -0
  75. package/framework/stacks/nextjs/error-handling.md +442 -0
  76. package/framework/stacks/nextjs/feedback.md +124 -0
  77. package/framework/stacks/nextjs/migrations.md +332 -0
  78. package/framework/stacks/nextjs/models.md +362 -0
  79. package/framework/stacks/nextjs/queries.md +410 -0
  80. package/framework/stacks/nextjs/responsive.md +338 -0
  81. package/framework/stacks/nextjs/tech-stack.md +177 -0
  82. package/framework/stacks/nextjs/test-writing.md +475 -0
  83. package/framework/stacks/nextjs/validation.md +467 -0
  84. package/framework/stacks/python/api.md +468 -0
  85. package/framework/stacks/python/authentication.md +342 -0
  86. package/framework/stacks/python/code-quality.md +283 -0
  87. package/framework/stacks/python/code-refactoring.md +315 -0
  88. package/framework/stacks/python/coding-style.md +462 -0
  89. package/framework/stacks/python/conventions.md +399 -0
  90. package/framework/stacks/python/error-handling.md +512 -0
  91. package/framework/stacks/python/feedback.md +92 -0
  92. package/framework/stacks/python/implement-ai-llm.md +468 -0
  93. package/framework/stacks/python/migrations.md +388 -0
  94. package/framework/stacks/python/models.md +399 -0
  95. package/framework/stacks/python/python.md +232 -0
  96. package/framework/stacks/python/queries.md +451 -0
  97. package/framework/stacks/python/structure.md +245 -58
  98. package/framework/stacks/python/tech.md +92 -35
  99. package/framework/stacks/python/testing.md +380 -0
  100. package/framework/stacks/python/validation.md +471 -0
  101. package/framework/stacks/rails/authentication.md +176 -0
  102. package/framework/stacks/rails/code-quality.md +287 -0
  103. package/framework/stacks/rails/code-refactoring.md +299 -0
  104. package/framework/stacks/rails/feedback.md +130 -0
  105. package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
  106. package/framework/stacks/rails/rails.md +301 -0
  107. package/framework/stacks/rails/rails8-best-practices.md +498 -0
  108. package/framework/stacks/rails/rails8-css.md +573 -0
  109. package/framework/stacks/rails/structure.md +140 -0
  110. package/framework/stacks/rails/tech.md +108 -0
  111. package/framework/stacks/react/code-quality.md +521 -0
  112. package/framework/stacks/react/components.md +625 -0
  113. package/framework/stacks/react/data-fetching.md +586 -0
  114. package/framework/stacks/react/feedback.md +110 -0
  115. package/framework/stacks/react/forms.md +694 -0
  116. package/framework/stacks/react/performance.md +640 -0
  117. package/framework/stacks/react/product.md +22 -9
  118. package/framework/stacks/react/state-management.md +472 -0
  119. package/framework/stacks/react/structure.md +351 -44
  120. package/framework/stacks/react/tech.md +219 -30
  121. package/framework/stacks/react/testing.md +690 -0
  122. package/package.json +1 -1
  123. package/framework/stacks/node/product.md +0 -27
  124. package/framework/stacks/node/structure.md +0 -82
  125. 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._