@mr.dj2u/knowledge 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/dist/content/checklists/ship-test-loop.md +16 -0
- package/dist/content/checklists/unified-agent-bundle-validation.md +8 -0
- package/dist/content/examples/ship-test-loop.md +13 -0
- package/dist/content/examples/unified-agent-bundle-bootstrap.md +8 -0
- package/dist/content/guides/animation-performance.md +30 -0
- package/dist/content/guides/post-create-onboarding.md +113 -0
- package/dist/content/patterns/api/api-routes.md +314 -0
- package/dist/content/patterns/api/error-handling.md +311 -0
- package/dist/content/patterns/database/drizzle-schema.md +280 -0
- package/dist/content/patterns/database/migrations.md +365 -0
- package/dist/content/patterns/database/query-organization.md +537 -0
- package/dist/content/patterns/database/relations.md +450 -0
- package/dist/content/patterns/deployment/build-configuration.md +452 -0
- package/dist/content/patterns/deployment/ci-cd-patterns.md +448 -0
- package/dist/content/patterns/deployment/environment-config.md +380 -0
- package/dist/content/patterns/deployment/hosting-setup.md +425 -0
- package/dist/content/patterns/project/configuration-patterns.md +459 -0
- package/dist/content/patterns/project/documentation-org.md +506 -0
- package/dist/content/patterns/project/folder-structure.md +398 -0
- package/dist/content/patterns/project/library-exports.md +465 -0
- package/dist/content/patterns/project/monorepo-structure.md +500 -0
- package/dist/content/patterns/routing/dynamic-routes.md +221 -0
- package/dist/content/patterns/routing/file-based-routing.md +186 -0
- package/dist/content/patterns/routing/route-groups.md +429 -0
- package/dist/content/patterns/state/persistence-middleware.md +521 -0
- package/dist/content/patterns/state/selector-hooks.md +538 -0
- package/dist/content/patterns/state/store-organization.md +539 -0
- package/dist/content/patterns/state/zustand-patterns.md +348 -0
- package/dist/content/patterns/styling/component-styling.md +468 -0
- package/dist/content/patterns/styling/responsive-patterns.md +398 -0
- package/dist/content/patterns/styling/theme-configuration.md +426 -0
- package/dist/content/patterns/styling/uniwind-setup.md +412 -0
- package/dist/content/prompts/continue-development.md +27 -0
- package/dist/content/prompts/create-expo-super-stack.md +29 -0
- package/dist/content/prompts/fix-seo.md +29 -0
- package/dist/content/prompts/onboard-new-expo-app.md +11 -0
- package/dist/content/prompts/prepare-deploy.md +29 -0
- package/dist/content/prompts/project-research-plan.md +29 -0
- package/dist/content/prompts/review-expo-project.md +29 -0
- package/dist/content/prompts/run-doctor.md +30 -0
- package/dist/content/prompts/ship-test-loop.md +24 -0
- package/dist/content/reference/create-expo-stack-uniwind.md +29 -0
- package/dist/content/reference/doctor-dogfood.md +42 -0
- package/dist/content/reference/mcp-sdk-transport.md +30 -0
- package/dist/content/reference/package-ci-patterns.md +24 -0
- package/dist/content/reference/reference-repo-evacuation.md +31 -0
- package/dist/content/resource-index.json +67 -0
- package/dist/content/rules/app-folder-architecture.md +13 -0
- package/dist/content/rules/env-hygiene.md +9 -0
- package/dist/content/rules/seo-metadata.md +7 -0
- package/dist/content/rules/ssr-safety.md +9 -0
- package/dist/content/skills/api-routes.md +33 -0
- package/dist/content/skills/continue-development.md +31 -0
- package/dist/content/skills/debugging.md +31 -0
- package/dist/content/skills/deployment.md +32 -0
- package/dist/content/skills/dev-server-management.md +31 -0
- package/dist/content/skills/env-vars.md +32 -0
- package/dist/content/skills/expo-router-architecture.md +33 -0
- package/dist/content/skills/expo-ssr-safety.md +32 -0
- package/dist/content/skills/plugin-creation.md +41 -0
- package/dist/content/skills/production-server-patterns.md +31 -0
- package/dist/content/skills/project-onboarding.md +31 -0
- package/dist/content/skills/research-plan-intake.md +31 -0
- package/dist/content/skills/seo-metadata.md +31 -0
- package/dist/content/skills/super-stack-startup.md +31 -0
- package/dist/content/skills/uniwind-theming.md +32 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +369 -0
- package/dist/index.js.map +1 -0
- package/dist/patterns/index.d.ts +78 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +264 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/prompts/index.d.ts +35 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +270 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/skills/index.d.ts +3 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/index.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Error Handling in API Services
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Structured error handling in API services uses custom error classes with status codes, error details, and proper HTTP response formatting. This ensures consistent error responses across all API endpoints with full context for debugging and client-side error handling.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
**Use custom error handling** for:
|
|
10
|
+
- ✅ API endpoints that need consistent error responses
|
|
11
|
+
- ✅ Services that interact with external APIs or databases
|
|
12
|
+
- ✅ Situations where you need to distinguish error types and statuses
|
|
13
|
+
- ✅ Client-side error handling with structured error objects
|
|
14
|
+
|
|
15
|
+
## Code Example
|
|
16
|
+
|
|
17
|
+
### Custom Error Class Definition
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// File: src/services/quantum-key-management.ts
|
|
21
|
+
export class QuantumApiError extends Error {
|
|
22
|
+
status: number;
|
|
23
|
+
details?: unknown;
|
|
24
|
+
|
|
25
|
+
constructor(message: string, status: number, details?: unknown) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'QuantumApiError';
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.details = details;
|
|
30
|
+
|
|
31
|
+
// Maintain proper prototype chain for instanceof checks
|
|
32
|
+
Object.setPrototypeOf(this, QuantumApiError.prototype);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Usage in services
|
|
37
|
+
async function fetchQuantumProfile(
|
|
38
|
+
bearerClient: QuantumApiClient,
|
|
39
|
+
profileId: string
|
|
40
|
+
): Promise<IbmProfileRecord> {
|
|
41
|
+
try {
|
|
42
|
+
const response = await bearerClient.getProfile(profileId);
|
|
43
|
+
return response;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err instanceof SdkQuantumApiError) {
|
|
46
|
+
throw new QuantumApiError(
|
|
47
|
+
`Failed to fetch profile: ${err.message}`,
|
|
48
|
+
err.statusCode || 500,
|
|
49
|
+
{ originalError: err }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
throw new QuantumApiError(
|
|
53
|
+
'Unknown error fetching profile',
|
|
54
|
+
500,
|
|
55
|
+
{ originalError: err }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**From:** DJsPortfolio/src/services/quantum-key-management.ts (lines 1-60)
|
|
62
|
+
|
|
63
|
+
### SDK Client Error Mapping
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// File: src/lib/quantum-sdk-executor.ts
|
|
67
|
+
import { QuantumApiError as SdkQuantumApiError } from '@mr.dj2u/quantum-api';
|
|
68
|
+
|
|
69
|
+
export type QuantumSdkEndpointExecutionResult = {
|
|
70
|
+
status: number;
|
|
71
|
+
statusText: string;
|
|
72
|
+
data: unknown;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
async function executeQuantumEndpoint(
|
|
76
|
+
input: QuantumSdkEndpointExecutionInput
|
|
77
|
+
): Promise<QuantumSdkEndpointExecutionResult> {
|
|
78
|
+
try {
|
|
79
|
+
// Select appropriate client based on auth method
|
|
80
|
+
const client = input.bearerToken
|
|
81
|
+
? createQuantumBearerClient(input.baseUrl, input.bearerToken)
|
|
82
|
+
: createQuantumPublicClient(input.baseUrl);
|
|
83
|
+
|
|
84
|
+
// Execute request
|
|
85
|
+
const response = await client.request({
|
|
86
|
+
method: input.method,
|
|
87
|
+
path: input.path,
|
|
88
|
+
body: input.body,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
status: response.status,
|
|
93
|
+
statusText: response.statusText,
|
|
94
|
+
data: response.data,
|
|
95
|
+
};
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err instanceof SdkQuantumApiError) {
|
|
98
|
+
return {
|
|
99
|
+
status: err.statusCode || 500,
|
|
100
|
+
statusText: 'Error',
|
|
101
|
+
data: {
|
|
102
|
+
error: err.message,
|
|
103
|
+
details: err.details,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
status: 500,
|
|
110
|
+
statusText: 'Internal Server Error',
|
|
111
|
+
data: {
|
|
112
|
+
error: 'Unexpected error executing endpoint',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**From:** DJsPortfolio/src/lib/quantum-sdk-executor.ts (lines 1-50)
|
|
120
|
+
|
|
121
|
+
### Error Response Format
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Standard error response structure
|
|
125
|
+
type ErrorResponse = {
|
|
126
|
+
error: string; // Human-readable error message
|
|
127
|
+
status: number; // HTTP status code
|
|
128
|
+
details?: unknown; // Additional error context
|
|
129
|
+
timestamp?: string; // When error occurred
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Return from API route
|
|
133
|
+
export async function POST(request: ExpoRequest) {
|
|
134
|
+
try {
|
|
135
|
+
const data = await request.json();
|
|
136
|
+
const result = await processQuantumRequest(data);
|
|
137
|
+
return new ExpoResponse(JSON.stringify(result), { status: 200 });
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const errorResponse: ErrorResponse = {
|
|
140
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
141
|
+
status: err instanceof QuantumApiError ? err.status : 500,
|
|
142
|
+
details: err instanceof QuantumApiError ? err.details : undefined,
|
|
143
|
+
timestamp: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return new ExpoResponse(
|
|
147
|
+
JSON.stringify(errorResponse),
|
|
148
|
+
{
|
|
149
|
+
status: errorResponse.status,
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
### Environment-Specific Error Handling
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// config/error-handling.ts
|
|
163
|
+
export const errorConfig = {
|
|
164
|
+
development: {
|
|
165
|
+
exposeStackTraces: true,
|
|
166
|
+
logErrors: true,
|
|
167
|
+
verboseMessages: true,
|
|
168
|
+
},
|
|
169
|
+
production: {
|
|
170
|
+
exposeStackTraces: false,
|
|
171
|
+
logErrors: true,
|
|
172
|
+
verboseMessages: false,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export function shouldExposeErrorDetails(): boolean {
|
|
177
|
+
return errorConfig[process.env.NODE_ENV || 'development'].exposeStackTraces;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Error Logging
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// middleware/error-logger.ts
|
|
185
|
+
export function logError(err: Error, context?: Record<string, any>) {
|
|
186
|
+
const timestamp = new Date().toISOString();
|
|
187
|
+
const errorData = {
|
|
188
|
+
timestamp,
|
|
189
|
+
message: err.message,
|
|
190
|
+
name: err.name,
|
|
191
|
+
stack: err.stack,
|
|
192
|
+
context,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
console.error(JSON.stringify(errorData));
|
|
196
|
+
|
|
197
|
+
// Could also send to external logging service
|
|
198
|
+
// sendToSentry(errorData);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Best Practices
|
|
203
|
+
|
|
204
|
+
### ✅ DO
|
|
205
|
+
|
|
206
|
+
1. **Create typed error classes** for different error scenarios
|
|
207
|
+
```typescript
|
|
208
|
+
class ValidationError extends Error {
|
|
209
|
+
constructor(public field: string, message: string) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = 'ValidationError';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
class AuthenticationError extends Error {
|
|
216
|
+
constructor(message: string) {
|
|
217
|
+
super(message);
|
|
218
|
+
this.name = 'AuthenticationError';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
2. **Include status codes** with all errors
|
|
224
|
+
```typescript
|
|
225
|
+
throw new ValidationError('email', 'Invalid email format');
|
|
226
|
+
// Map to 400 status in handler
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
3. **Log errors with context** for debugging
|
|
230
|
+
```typescript
|
|
231
|
+
try {
|
|
232
|
+
await fetchData();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logError(err, {
|
|
235
|
+
userId: user.id,
|
|
236
|
+
action: 'fetch-profile',
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
4. **Distinguish error types** in client responses
|
|
243
|
+
```typescript
|
|
244
|
+
const statusMap = {
|
|
245
|
+
ValidationError: 400,
|
|
246
|
+
AuthenticationError: 401,
|
|
247
|
+
NotFoundError: 404,
|
|
248
|
+
ServerError: 500,
|
|
249
|
+
};
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### ❌ DON'T
|
|
253
|
+
|
|
254
|
+
1. **Don't expose internal error details** in production
|
|
255
|
+
```typescript
|
|
256
|
+
// ❌ BAD - leaks implementation details
|
|
257
|
+
return JSON.stringify({ error: err.stack });
|
|
258
|
+
|
|
259
|
+
// ✅ GOOD - generic message, log details server-side
|
|
260
|
+
return JSON.stringify({ error: 'Internal server error' });
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
2. **Don't ignore error types** from third-party libraries
|
|
264
|
+
```typescript
|
|
265
|
+
// ❌ BAD - loses error context
|
|
266
|
+
catch (err) {
|
|
267
|
+
throw new Error('Failed');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ✅ GOOD - map SDK errors to application errors
|
|
271
|
+
catch (err) {
|
|
272
|
+
if (err instanceof SdkError) {
|
|
273
|
+
throw new ApiError(err.message, err.statusCode);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
3. **Don't return different error formats** across endpoints
|
|
279
|
+
```typescript
|
|
280
|
+
// ❌ INCONSISTENT
|
|
281
|
+
Endpoint 1: { message: "Error" }
|
|
282
|
+
Endpoint 2: { error: "Error" }
|
|
283
|
+
Endpoint 3: { err: "Error" }
|
|
284
|
+
|
|
285
|
+
// ✅ CONSISTENT
|
|
286
|
+
All endpoints: { error, status, details, timestamp }
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
4. **Don't forget to set proper HTTP status codes**
|
|
290
|
+
```typescript
|
|
291
|
+
// ❌ BAD - always returns 200
|
|
292
|
+
return new ExpoResponse(JSON.stringify(error), { status: 200 });
|
|
293
|
+
|
|
294
|
+
// ✅ GOOD - correct status codes
|
|
295
|
+
return new ExpoResponse(
|
|
296
|
+
JSON.stringify(error),
|
|
297
|
+
{ status: error.status || 500 }
|
|
298
|
+
);
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Related Patterns
|
|
302
|
+
|
|
303
|
+
- [API Routes](./api-routes.md) — Route endpoint structure
|
|
304
|
+
- [CORS Configuration](./cors-configuration.md) — Origin validation
|
|
305
|
+
- [Health Endpoints](./health-endpoints.md) — Health check patterns
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
*Pattern extracted from production repositories: DJsPortfolio, PokePages*
|
|
310
|
+
*Files: DJsPortfolio/src\services\quantum-key-management.ts*
|
|
311
|
+
*Lines 1-60 showing custom QuantumApiError class and error handling patterns*
|
|
@@ -0,0 +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*
|
|
280
|
+
*Lines 1-69 with full JSONB payload handling and check constraints*
|