@mr.dj2u/knowledge 0.1.6 → 0.1.8
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/dist/content/checklists/push-merge-loop.md +17 -17
- package/dist/content/checklists/unified-agent-bundle-validation.md +9 -9
- package/dist/content/examples/push-merge-loop.md +14 -14
- package/dist/content/examples/unified-agent-bundle-bootstrap.md +8 -8
- package/dist/content/guides/animation-performance.md +31 -1
- package/dist/content/guides/post-create-onboarding.md +140 -140
- package/dist/content/patterns/animation/animation-motion-selection.md +155 -0
- package/dist/content/patterns/api/api-routes.md +313 -313
- package/dist/content/patterns/api/error-handling.md +310 -310
- package/dist/content/patterns/database/drizzle-schema.md +279 -279
- package/dist/content/patterns/database/migrations.md +364 -364
- package/dist/content/patterns/database/query-organization.md +536 -536
- package/dist/content/patterns/database/relations.md +449 -449
- package/dist/content/patterns/deployment/build-configuration.md +440 -440
- package/dist/content/patterns/deployment/ci-cd-patterns.md +447 -447
- package/dist/content/patterns/deployment/environment-config.md +379 -379
- package/dist/content/patterns/deployment/hosting-setup.md +424 -424
- package/dist/content/patterns/project/configuration-patterns.md +459 -459
- package/dist/content/patterns/project/documentation-org.md +506 -506
- package/dist/content/patterns/project/folder-structure.md +397 -397
- package/dist/content/patterns/project/library-exports.md +464 -464
- package/dist/content/patterns/project/monorepo-structure.md +500 -500
- package/dist/content/patterns/routing/dynamic-routes.md +220 -220
- package/dist/content/patterns/routing/file-based-routing.md +185 -185
- package/dist/content/patterns/routing/route-groups.md +428 -428
- package/dist/content/patterns/state/persistence-middleware.md +520 -520
- package/dist/content/patterns/state/selector-hooks.md +537 -537
- package/dist/content/patterns/state/store-organization.md +538 -538
- package/dist/content/patterns/state/zustand-patterns.md +347 -347
- package/dist/content/patterns/styling/component-styling.md +467 -467
- package/dist/content/patterns/styling/responsive-patterns.md +397 -397
- package/dist/content/patterns/styling/theme-configuration.md +425 -425
- package/dist/content/patterns/styling/uniwind-setup.md +411 -411
- package/dist/content/prompts/continue-development.md +35 -35
- package/dist/content/prompts/fix-seo.md +29 -29
- package/dist/content/prompts/onboard-new-expo-app.md +11 -11
- package/dist/content/prompts/prepare-deploy.md +29 -29
- package/dist/content/prompts/project-research-plan.md +29 -29
- package/dist/content/prompts/push-merge-loop.md +25 -25
- package/dist/content/prompts/review-expo-project.md +29 -29
- package/dist/content/prompts/review-motion.md +58 -0
- package/dist/content/prompts/run-doctor.md +38 -38
- package/dist/content/prompts/wrap-up.md +67 -67
- package/dist/content/reference/create-expo-stack-uniwind.md +29 -29
- package/dist/content/reference/mcp-sdk-transport.md +30 -30
- package/dist/content/reference/reference-repo-evacuation.md +31 -31
- package/dist/content/resource-index.json +3 -0
- package/dist/content/skills/animation-motion.md +68 -0
- package/dist/content/skills/api-routes.md +33 -33
- package/dist/content/skills/continue-development.md +32 -32
- package/dist/content/skills/debugging.md +32 -32
- package/dist/content/skills/deployment.md +32 -32
- package/dist/content/skills/dev-server-management.md +32 -32
- package/dist/content/skills/env-vars.md +32 -32
- package/dist/content/skills/expo-router-architecture.md +33 -33
- package/dist/content/skills/expo-ssr-safety.md +32 -32
- package/dist/content/skills/plugin-creation.md +41 -41
- package/dist/content/skills/production-server-patterns.md +31 -31
- package/dist/content/skills/project-onboarding.md +31 -31
- package/dist/content/skills/research-plan-intake.md +32 -32
- package/dist/content/skills/seo-metadata.md +31 -31
- package/dist/content/skills/super-stack-startup.md +34 -34
- package/dist/content/skills/uniwind-theming.md +32 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -1
- package/dist/patterns/index.d.ts +9 -1
- package/dist/patterns/index.d.ts.map +1 -1
- package/dist/patterns/index.js +17 -0
- package/dist/patterns/index.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +17 -0
- package/dist/prompts/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -1,280 +1,280 @@
|
|
|
1
|
-
# Drizzle ORM Schema Design
|
|
2
|
-
|
|
3
|
-
## Description
|
|
4
|
-
|
|
5
|
-
Drizzle ORM schemas define type-safe database tables with full TypeScript inference from database types to application types. Schemas use domain-specific files with shared relations.ts for foreign keys and relationships, enabling compile-time type safety and automatic type generation.
|
|
6
|
-
|
|
7
|
-
## When to Use
|
|
8
|
-
|
|
9
|
-
**Use Drizzle schemas** for:
|
|
10
|
-
- ✅ Any PostgreSQL table definition in React Native/Node.js applications
|
|
11
|
-
- ✅ Projects requiring full TypeScript type inference (no separate type definitions)
|
|
12
|
-
- ✅ Applications with complex relationships and joins
|
|
13
|
-
- ✅ Teams valuing compile-time safety over runtime convenience
|
|
14
|
-
|
|
15
|
-
## Code Example
|
|
16
|
-
|
|
17
|
-
### Basic Schema with Type Inference
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
// File: packages/db/src/schema/quantum-api/quantum-execution-jobs/schema.ts
|
|
21
|
-
import { pgTable, uuid, text, jsonb, integer, timestamp, unique, check } from 'drizzle-orm/pg-core';
|
|
22
|
-
|
|
23
|
-
export const quantumExecutionJobs = pgTable('quantum_execution_jobs', {
|
|
24
|
-
id: uuid('id').defaultRandom().primaryKey(),
|
|
25
|
-
provider: text('provider').notNull(), // 'ibm' | 'aws' | etc.
|
|
26
|
-
remoteJobId: text('remote_job_id').notNull(),
|
|
27
|
-
status: text('status').notNull().default('queued'), // queued | running | succeeded | failed | cancelled
|
|
28
|
-
requestPayload: jsonb('request_payload').notNull(), // Full request object
|
|
29
|
-
resultPayload: jsonb('result_payload'), // Result when status === succeeded
|
|
30
|
-
errorPayload: jsonb('error_payload'), // Error object when status === failed
|
|
31
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
32
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
33
|
-
completedAt: timestamp('completed_at'),
|
|
34
|
-
}, (table) => ({
|
|
35
|
-
// Composite unique constraint
|
|
36
|
-
uniqueRemoteJob: unique().on(table.provider, table.remoteJobId),
|
|
37
|
-
|
|
38
|
-
// Check constraints for valid status values
|
|
39
|
-
statusCheck: check(
|
|
40
|
-
'status_check',
|
|
41
|
-
sql`status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')`
|
|
42
|
-
),
|
|
43
|
-
|
|
44
|
-
// Ensure completedAt is set when status is terminal
|
|
45
|
-
completedAtCheck: check(
|
|
46
|
-
'completed_at_check',
|
|
47
|
-
sql`(status IN ('succeeded', 'failed', 'cancelled') AND completed_at IS NOT NULL)
|
|
48
|
-
OR (status NOT IN ('succeeded', 'failed', 'cancelled') AND completed_at IS NULL)`
|
|
49
|
-
),
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
// Infer types from schema
|
|
53
|
-
export type QuantumExecutionJob = typeof quantumExecutionJobs.$inferSelect;
|
|
54
|
-
export type NewQuantumExecutionJob = typeof quantumExecutionJobs.$inferInsert;
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**From:** DJsPortfolio/packages/db/src/schema/quantum-api/quantum-execution-jobs/schema.ts (lines 1-69)
|
|
58
|
-
|
|
59
|
-
### Schema with Foreign Keys
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
// File: src/db/eventClaimsSchema.ts (PokePages pattern)
|
|
63
|
-
import { pgTable, uuid, text, timestamp, integer, foreignKey } from 'drizzle-orm/pg-core';
|
|
64
|
-
import { profiles } from './profilesSchema';
|
|
65
|
-
import { events } from './eventsSchema';
|
|
66
|
-
|
|
67
|
-
export const eventClaims = pgTable(
|
|
68
|
-
'event_claims',
|
|
69
|
-
{
|
|
70
|
-
id: uuid('id').defaultRandom().primaryKey(),
|
|
71
|
-
userId: uuid('user_id').notNull(),
|
|
72
|
-
eventId: uuid('event_id').notNull(),
|
|
73
|
-
claimedAt: timestamp('claimed_at').defaultNow().notNull(),
|
|
74
|
-
proofData: text('proof_data'), // Optional proof/screenshot
|
|
75
|
-
},
|
|
76
|
-
(table) => ({
|
|
77
|
-
userFk: foreignKey({
|
|
78
|
-
columns: [table.userId],
|
|
79
|
-
foreignColumns: [profiles.id],
|
|
80
|
-
name: 'event_claims_user_fk',
|
|
81
|
-
}).onDelete('cascade'),
|
|
82
|
-
|
|
83
|
-
eventFk: foreignKey({
|
|
84
|
-
columns: [table.eventId],
|
|
85
|
-
foreignColumns: [events.id],
|
|
86
|
-
name: 'event_claims_event_fk',
|
|
87
|
-
}).onDelete('cascade'),
|
|
88
|
-
|
|
89
|
-
// Unique constraint: user can only claim event once
|
|
90
|
-
uniqueUserEvent: unique().on(table.userId, table.eventId),
|
|
91
|
-
})
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
export type EventClaim = typeof eventClaims.$inferSelect;
|
|
95
|
-
export type NewEventClaim = typeof eventClaims.$inferInsert;
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
**From:** PokePages/src/db/eventClaimsSchema.ts pattern
|
|
99
|
-
|
|
100
|
-
### Indexed Schema for Performance
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// File: src/db/postsSchema.ts
|
|
104
|
-
import { pgTable, uuid, text, timestamp, integer, index } from 'drizzle-orm/pg-core';
|
|
105
|
-
|
|
106
|
-
export const posts = pgTable(
|
|
107
|
-
'posts',
|
|
108
|
-
{
|
|
109
|
-
id: uuid('id').defaultRandom().primaryKey(),
|
|
110
|
-
userId: uuid('user_id').notNull(),
|
|
111
|
-
title: text('title').notNull(),
|
|
112
|
-
content: text('content').notNull(),
|
|
113
|
-
likes: integer('likes').default(0),
|
|
114
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
115
|
-
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
116
|
-
deletedAt: timestamp('deleted_at'), // Soft delete
|
|
117
|
-
},
|
|
118
|
-
(table) => ({
|
|
119
|
-
// Single column indexes for common queries
|
|
120
|
-
userIdIdx: index('posts_user_id_idx').on(table.userId),
|
|
121
|
-
createdAtIdx: index('posts_created_at_idx').on(table.createdAt),
|
|
122
|
-
|
|
123
|
-
// Composite index for common query pattern
|
|
124
|
-
userCreatedIdx: index('posts_user_created_idx')
|
|
125
|
-
.on(table.userId, table.createdAt),
|
|
126
|
-
|
|
127
|
-
// Index for soft deletes
|
|
128
|
-
activePostsIdx: index('posts_active_idx')
|
|
129
|
-
.on(table.userId, table.deletedAt),
|
|
130
|
-
})
|
|
131
|
-
);
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Configuration
|
|
135
|
-
|
|
136
|
-
### Database Connection Setup
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
// File: src/db/index.ts
|
|
140
|
-
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
141
|
-
import postgres from 'postgres';
|
|
142
|
-
import * as schemas from './schemas';
|
|
143
|
-
|
|
144
|
-
const connectionString = process.env.DATABASE_URL!;
|
|
145
|
-
|
|
146
|
-
// Optimized connection pool for Supabase PgBouncer
|
|
147
|
-
export const client = postgres(connectionString, {
|
|
148
|
-
prepare: false, // Required for PgBouncer
|
|
149
|
-
ssl: 'require',
|
|
150
|
-
max: 3, // Small pool size
|
|
151
|
-
idle_timeout: 20, // Close idle connections
|
|
152
|
-
connect_timeout: 30, // Connection timeout
|
|
153
|
-
max_lifetime: 1800, // Recycle after 30 minutes
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
export const db = drizzle(client, {
|
|
157
|
-
schema: schemas,
|
|
158
|
-
});
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### TypeScript Configuration
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
// File: tsconfig.json
|
|
165
|
-
{
|
|
166
|
-
"compilerOptions": {
|
|
167
|
-
"plugins": [
|
|
168
|
-
{
|
|
169
|
-
"name": "drizzle-orm/compiler",
|
|
170
|
-
"options": {
|
|
171
|
-
"casing": "snake_case"
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
]
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
## Best Practices
|
|
180
|
-
|
|
181
|
-
### ✅ DO
|
|
182
|
-
|
|
183
|
-
1. **Infer types from schema** — don't maintain separate type definitions
|
|
184
|
-
```typescript
|
|
185
|
-
// ✅ GOOD - single source of truth
|
|
186
|
-
export type Post = typeof posts.$inferSelect;
|
|
187
|
-
export type NewPost = typeof posts.$inferInsert;
|
|
188
|
-
|
|
189
|
-
// ❌ BAD - duplicate type definitions
|
|
190
|
-
interface Post {
|
|
191
|
-
id: string;
|
|
192
|
-
title: string;
|
|
193
|
-
// ... manually maintaining type sync
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
2. **Use domain-specific schema files**
|
|
198
|
-
```
|
|
199
|
-
src/db/
|
|
200
|
-
├── profilesSchema.ts
|
|
201
|
-
├── postsSchema.ts
|
|
202
|
-
├── commentsSchema.ts
|
|
203
|
-
├── relations.ts ← all relationships defined here
|
|
204
|
-
└── index.ts
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
3. **Define relationships in shared relations.ts**
|
|
208
|
-
```typescript
|
|
209
|
-
// File: src/db/relations.ts
|
|
210
|
-
import { relations } from 'drizzle-orm';
|
|
211
|
-
import { profiles, posts, comments } from './schemas';
|
|
212
|
-
|
|
213
|
-
export const profilesRelations = relations(profiles, ({ many }) => ({
|
|
214
|
-
posts: many(posts),
|
|
215
|
-
}));
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
4. **Use JSONB for flexible data** instead of many normalized tables
|
|
219
|
-
```typescript
|
|
220
|
-
requestPayload: jsonb('request_payload').notNull(),
|
|
221
|
-
resultPayload: jsonb('result_payload'),
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
5. **Add check constraints** for data integrity
|
|
225
|
-
```typescript
|
|
226
|
-
check(
|
|
227
|
-
'status_check',
|
|
228
|
-
sql`status IN ('pending', 'completed', 'failed')`
|
|
229
|
-
)
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### ❌ DON'T
|
|
233
|
-
|
|
234
|
-
1. **Don't create overly normalized schemas** when embedding is more performant
|
|
235
|
-
```typescript
|
|
236
|
-
// ❌ AVOID - creates N+1 queries
|
|
237
|
-
users table → user_profiles table → user_settings table
|
|
238
|
-
|
|
239
|
-
// ✅ BETTER - use JSONB for related data
|
|
240
|
-
users: { profile: jsonb, settings: jsonb }
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
2. **Don't forget indexes** on frequently queried columns
|
|
244
|
-
```typescript
|
|
245
|
-
// ❌ BAD - no index on userId
|
|
246
|
-
posts table with userId but no index
|
|
247
|
-
|
|
248
|
-
// ✅ GOOD - indexed for fast lookups
|
|
249
|
-
userIdIdx: index().on(table.userId)
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
3. **Don't use mutable defaults** (like functions) for timestamps
|
|
253
|
-
```typescript
|
|
254
|
-
// ❌ BAD - function called once at migration time
|
|
255
|
-
createdAt: timestamp('created_at').default(fn)
|
|
256
|
-
|
|
257
|
-
// ✅ GOOD - database handles default for each row
|
|
258
|
-
createdAt: timestamp('created_at').defaultNow()
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
4. **Don't create tables without soft delete** if you might need historical data
|
|
262
|
-
```typescript
|
|
263
|
-
// ✅ GOOD - reversible deletion
|
|
264
|
-
deletedAt: timestamp('deleted_at')
|
|
265
|
-
|
|
266
|
-
// Query active records
|
|
267
|
-
where(isNull(posts.deletedAt))
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
## Related Patterns
|
|
271
|
-
|
|
272
|
-
- [Database Migrations](./migrations.md) — Managing schema changes
|
|
273
|
-
- [Relationships](./relations.md) — Foreign keys and joins
|
|
274
|
-
- [Query Organization](./query-organization.md) — Data access layer
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
*Pattern extracted from production repositories: DJsPortfolio, PokePages, core-monorepo*
|
|
279
|
-
*Files: DJsPortfolio/packages\db\src\schema\quantum-api\quantum-execution-jobs\schema.ts*
|
|
1
|
+
# Drizzle ORM Schema Design
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Drizzle ORM schemas define type-safe database tables with full TypeScript inference from database types to application types. Schemas use domain-specific files with shared relations.ts for foreign keys and relationships, enabling compile-time type safety and automatic type generation.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
**Use Drizzle schemas** for:
|
|
10
|
+
- ✅ Any PostgreSQL table definition in React Native/Node.js applications
|
|
11
|
+
- ✅ Projects requiring full TypeScript type inference (no separate type definitions)
|
|
12
|
+
- ✅ Applications with complex relationships and joins
|
|
13
|
+
- ✅ Teams valuing compile-time safety over runtime convenience
|
|
14
|
+
|
|
15
|
+
## Code Example
|
|
16
|
+
|
|
17
|
+
### Basic Schema with Type Inference
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// File: packages/db/src/schema/quantum-api/quantum-execution-jobs/schema.ts
|
|
21
|
+
import { pgTable, uuid, text, jsonb, integer, timestamp, unique, check } from 'drizzle-orm/pg-core';
|
|
22
|
+
|
|
23
|
+
export const quantumExecutionJobs = pgTable('quantum_execution_jobs', {
|
|
24
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
25
|
+
provider: text('provider').notNull(), // 'ibm' | 'aws' | etc.
|
|
26
|
+
remoteJobId: text('remote_job_id').notNull(),
|
|
27
|
+
status: text('status').notNull().default('queued'), // queued | running | succeeded | failed | cancelled
|
|
28
|
+
requestPayload: jsonb('request_payload').notNull(), // Full request object
|
|
29
|
+
resultPayload: jsonb('result_payload'), // Result when status === succeeded
|
|
30
|
+
errorPayload: jsonb('error_payload'), // Error object when status === failed
|
|
31
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
32
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
33
|
+
completedAt: timestamp('completed_at'),
|
|
34
|
+
}, (table) => ({
|
|
35
|
+
// Composite unique constraint
|
|
36
|
+
uniqueRemoteJob: unique().on(table.provider, table.remoteJobId),
|
|
37
|
+
|
|
38
|
+
// Check constraints for valid status values
|
|
39
|
+
statusCheck: check(
|
|
40
|
+
'status_check',
|
|
41
|
+
sql`status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')`
|
|
42
|
+
),
|
|
43
|
+
|
|
44
|
+
// Ensure completedAt is set when status is terminal
|
|
45
|
+
completedAtCheck: check(
|
|
46
|
+
'completed_at_check',
|
|
47
|
+
sql`(status IN ('succeeded', 'failed', 'cancelled') AND completed_at IS NOT NULL)
|
|
48
|
+
OR (status NOT IN ('succeeded', 'failed', 'cancelled') AND completed_at IS NULL)`
|
|
49
|
+
),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Infer types from schema
|
|
53
|
+
export type QuantumExecutionJob = typeof quantumExecutionJobs.$inferSelect;
|
|
54
|
+
export type NewQuantumExecutionJob = typeof quantumExecutionJobs.$inferInsert;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**From:** DJsPortfolio/packages/db/src/schema/quantum-api/quantum-execution-jobs/schema.ts (lines 1-69)
|
|
58
|
+
|
|
59
|
+
### Schema with Foreign Keys
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// File: src/db/eventClaimsSchema.ts (PokePages pattern)
|
|
63
|
+
import { pgTable, uuid, text, timestamp, integer, foreignKey } from 'drizzle-orm/pg-core';
|
|
64
|
+
import { profiles } from './profilesSchema';
|
|
65
|
+
import { events } from './eventsSchema';
|
|
66
|
+
|
|
67
|
+
export const eventClaims = pgTable(
|
|
68
|
+
'event_claims',
|
|
69
|
+
{
|
|
70
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
71
|
+
userId: uuid('user_id').notNull(),
|
|
72
|
+
eventId: uuid('event_id').notNull(),
|
|
73
|
+
claimedAt: timestamp('claimed_at').defaultNow().notNull(),
|
|
74
|
+
proofData: text('proof_data'), // Optional proof/screenshot
|
|
75
|
+
},
|
|
76
|
+
(table) => ({
|
|
77
|
+
userFk: foreignKey({
|
|
78
|
+
columns: [table.userId],
|
|
79
|
+
foreignColumns: [profiles.id],
|
|
80
|
+
name: 'event_claims_user_fk',
|
|
81
|
+
}).onDelete('cascade'),
|
|
82
|
+
|
|
83
|
+
eventFk: foreignKey({
|
|
84
|
+
columns: [table.eventId],
|
|
85
|
+
foreignColumns: [events.id],
|
|
86
|
+
name: 'event_claims_event_fk',
|
|
87
|
+
}).onDelete('cascade'),
|
|
88
|
+
|
|
89
|
+
// Unique constraint: user can only claim event once
|
|
90
|
+
uniqueUserEvent: unique().on(table.userId, table.eventId),
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
export type EventClaim = typeof eventClaims.$inferSelect;
|
|
95
|
+
export type NewEventClaim = typeof eventClaims.$inferInsert;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**From:** PokePages/src/db/eventClaimsSchema.ts pattern
|
|
99
|
+
|
|
100
|
+
### Indexed Schema for Performance
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// File: src/db/postsSchema.ts
|
|
104
|
+
import { pgTable, uuid, text, timestamp, integer, index } from 'drizzle-orm/pg-core';
|
|
105
|
+
|
|
106
|
+
export const posts = pgTable(
|
|
107
|
+
'posts',
|
|
108
|
+
{
|
|
109
|
+
id: uuid('id').defaultRandom().primaryKey(),
|
|
110
|
+
userId: uuid('user_id').notNull(),
|
|
111
|
+
title: text('title').notNull(),
|
|
112
|
+
content: text('content').notNull(),
|
|
113
|
+
likes: integer('likes').default(0),
|
|
114
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
115
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
116
|
+
deletedAt: timestamp('deleted_at'), // Soft delete
|
|
117
|
+
},
|
|
118
|
+
(table) => ({
|
|
119
|
+
// Single column indexes for common queries
|
|
120
|
+
userIdIdx: index('posts_user_id_idx').on(table.userId),
|
|
121
|
+
createdAtIdx: index('posts_created_at_idx').on(table.createdAt),
|
|
122
|
+
|
|
123
|
+
// Composite index for common query pattern
|
|
124
|
+
userCreatedIdx: index('posts_user_created_idx')
|
|
125
|
+
.on(table.userId, table.createdAt),
|
|
126
|
+
|
|
127
|
+
// Index for soft deletes
|
|
128
|
+
activePostsIdx: index('posts_active_idx')
|
|
129
|
+
.on(table.userId, table.deletedAt),
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
### Database Connection Setup
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// File: src/db/index.ts
|
|
140
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
141
|
+
import postgres from 'postgres';
|
|
142
|
+
import * as schemas from './schemas';
|
|
143
|
+
|
|
144
|
+
const connectionString = process.env.DATABASE_URL!;
|
|
145
|
+
|
|
146
|
+
// Optimized connection pool for Supabase PgBouncer
|
|
147
|
+
export const client = postgres(connectionString, {
|
|
148
|
+
prepare: false, // Required for PgBouncer
|
|
149
|
+
ssl: 'require',
|
|
150
|
+
max: 3, // Small pool size
|
|
151
|
+
idle_timeout: 20, // Close idle connections
|
|
152
|
+
connect_timeout: 30, // Connection timeout
|
|
153
|
+
max_lifetime: 1800, // Recycle after 30 minutes
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const db = drizzle(client, {
|
|
157
|
+
schema: schemas,
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### TypeScript Configuration
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// File: tsconfig.json
|
|
165
|
+
{
|
|
166
|
+
"compilerOptions": {
|
|
167
|
+
"plugins": [
|
|
168
|
+
{
|
|
169
|
+
"name": "drizzle-orm/compiler",
|
|
170
|
+
"options": {
|
|
171
|
+
"casing": "snake_case"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Best Practices
|
|
180
|
+
|
|
181
|
+
### ✅ DO
|
|
182
|
+
|
|
183
|
+
1. **Infer types from schema** — don't maintain separate type definitions
|
|
184
|
+
```typescript
|
|
185
|
+
// ✅ GOOD - single source of truth
|
|
186
|
+
export type Post = typeof posts.$inferSelect;
|
|
187
|
+
export type NewPost = typeof posts.$inferInsert;
|
|
188
|
+
|
|
189
|
+
// ❌ BAD - duplicate type definitions
|
|
190
|
+
interface Post {
|
|
191
|
+
id: string;
|
|
192
|
+
title: string;
|
|
193
|
+
// ... manually maintaining type sync
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
2. **Use domain-specific schema files**
|
|
198
|
+
```
|
|
199
|
+
src/db/
|
|
200
|
+
├── profilesSchema.ts
|
|
201
|
+
├── postsSchema.ts
|
|
202
|
+
├── commentsSchema.ts
|
|
203
|
+
├── relations.ts ← all relationships defined here
|
|
204
|
+
└── index.ts
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
3. **Define relationships in shared relations.ts**
|
|
208
|
+
```typescript
|
|
209
|
+
// File: src/db/relations.ts
|
|
210
|
+
import { relations } from 'drizzle-orm';
|
|
211
|
+
import { profiles, posts, comments } from './schemas';
|
|
212
|
+
|
|
213
|
+
export const profilesRelations = relations(profiles, ({ many }) => ({
|
|
214
|
+
posts: many(posts),
|
|
215
|
+
}));
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
4. **Use JSONB for flexible data** instead of many normalized tables
|
|
219
|
+
```typescript
|
|
220
|
+
requestPayload: jsonb('request_payload').notNull(),
|
|
221
|
+
resultPayload: jsonb('result_payload'),
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
5. **Add check constraints** for data integrity
|
|
225
|
+
```typescript
|
|
226
|
+
check(
|
|
227
|
+
'status_check',
|
|
228
|
+
sql`status IN ('pending', 'completed', 'failed')`
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### ❌ DON'T
|
|
233
|
+
|
|
234
|
+
1. **Don't create overly normalized schemas** when embedding is more performant
|
|
235
|
+
```typescript
|
|
236
|
+
// ❌ AVOID - creates N+1 queries
|
|
237
|
+
users table → user_profiles table → user_settings table
|
|
238
|
+
|
|
239
|
+
// ✅ BETTER - use JSONB for related data
|
|
240
|
+
users: { profile: jsonb, settings: jsonb }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
2. **Don't forget indexes** on frequently queried columns
|
|
244
|
+
```typescript
|
|
245
|
+
// ❌ BAD - no index on userId
|
|
246
|
+
posts table with userId but no index
|
|
247
|
+
|
|
248
|
+
// ✅ GOOD - indexed for fast lookups
|
|
249
|
+
userIdIdx: index().on(table.userId)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
3. **Don't use mutable defaults** (like functions) for timestamps
|
|
253
|
+
```typescript
|
|
254
|
+
// ❌ BAD - function called once at migration time
|
|
255
|
+
createdAt: timestamp('created_at').default(fn)
|
|
256
|
+
|
|
257
|
+
// ✅ GOOD - database handles default for each row
|
|
258
|
+
createdAt: timestamp('created_at').defaultNow()
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
4. **Don't create tables without soft delete** if you might need historical data
|
|
262
|
+
```typescript
|
|
263
|
+
// ✅ GOOD - reversible deletion
|
|
264
|
+
deletedAt: timestamp('deleted_at')
|
|
265
|
+
|
|
266
|
+
// Query active records
|
|
267
|
+
where(isNull(posts.deletedAt))
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Related Patterns
|
|
271
|
+
|
|
272
|
+
- [Database Migrations](./migrations.md) — Managing schema changes
|
|
273
|
+
- [Relationships](./relations.md) — Foreign keys and joins
|
|
274
|
+
- [Query Organization](./query-organization.md) — Data access layer
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
*Pattern extracted from production repositories: DJsPortfolio, PokePages, core-monorepo*
|
|
279
|
+
*Files: DJsPortfolio/packages\db\src\schema\quantum-api\quantum-execution-jobs\schema.ts*
|
|
280
280
|
*Lines 1-69 with full JSONB payload handling and check constraints*
|