@amirulabu/create-recurring-rabbit-app 0.0.0-alpha
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/bin/index.js +2 -0
- package/dist/index.js +592 -0
- package/package.json +43 -0
- package/templates/default/.editorconfig +21 -0
- package/templates/default/.env.example +15 -0
- package/templates/default/.eslintrc.json +35 -0
- package/templates/default/.prettierrc.json +7 -0
- package/templates/default/README.md +346 -0
- package/templates/default/app.config.ts +20 -0
- package/templates/default/docs/adding-features.md +439 -0
- package/templates/default/docs/adr/001-use-sqlite-for-development-database.md +22 -0
- package/templates/default/docs/adr/002-use-tanstack-start-over-nextjs.md +22 -0
- package/templates/default/docs/adr/003-use-better-auth-over-nextauth.md +22 -0
- package/templates/default/docs/adr/004-use-drizzle-over-prisma.md +22 -0
- package/templates/default/docs/adr/005-use-trpc-for-api-layer.md +22 -0
- package/templates/default/docs/adr/006-use-tailwind-css-v4-with-shadcn-ui.md +22 -0
- package/templates/default/docs/architecture.md +241 -0
- package/templates/default/docs/database.md +376 -0
- package/templates/default/docs/deployment.md +435 -0
- package/templates/default/docs/troubleshooting.md +668 -0
- package/templates/default/drizzle/migrations/0001_initial_schema.sql +39 -0
- package/templates/default/drizzle/migrations/meta/0001_snapshot.json +225 -0
- package/templates/default/drizzle/migrations/meta/_journal.json +12 -0
- package/templates/default/drizzle.config.ts +10 -0
- package/templates/default/lighthouserc.json +78 -0
- package/templates/default/src/app/__root.tsx +32 -0
- package/templates/default/src/app/api/auth/$.ts +15 -0
- package/templates/default/src/app/api/trpc.server.ts +12 -0
- package/templates/default/src/app/auth/forgot-password.tsx +107 -0
- package/templates/default/src/app/auth/login.tsx +34 -0
- package/templates/default/src/app/auth/register.tsx +34 -0
- package/templates/default/src/app/auth/reset-password.tsx +171 -0
- package/templates/default/src/app/auth/verify-email.tsx +111 -0
- package/templates/default/src/app/dashboard/index.tsx +122 -0
- package/templates/default/src/app/dashboard/settings.tsx +161 -0
- package/templates/default/src/app/globals.css +55 -0
- package/templates/default/src/app/index.tsx +83 -0
- package/templates/default/src/components/features/auth/login-form.tsx +172 -0
- package/templates/default/src/components/features/auth/register-form.tsx +202 -0
- package/templates/default/src/components/layout/dashboard-layout.tsx +27 -0
- package/templates/default/src/components/layout/header.tsx +29 -0
- package/templates/default/src/components/layout/sidebar.tsx +38 -0
- package/templates/default/src/components/ui/button.tsx +57 -0
- package/templates/default/src/components/ui/card.tsx +79 -0
- package/templates/default/src/components/ui/input.tsx +24 -0
- package/templates/default/src/lib/api.ts +42 -0
- package/templates/default/src/lib/auth.ts +292 -0
- package/templates/default/src/lib/email.ts +221 -0
- package/templates/default/src/lib/env.ts +119 -0
- package/templates/default/src/lib/hydration-timing.ts +289 -0
- package/templates/default/src/lib/monitoring.ts +336 -0
- package/templates/default/src/lib/utils.ts +6 -0
- package/templates/default/src/server/api/root.ts +10 -0
- package/templates/default/src/server/api/routers/dashboard.ts +37 -0
- package/templates/default/src/server/api/routers/user.ts +31 -0
- package/templates/default/src/server/api/trpc.ts +132 -0
- package/templates/default/src/server/auth/config.ts +241 -0
- package/templates/default/src/server/db/index.ts +153 -0
- package/templates/default/src/server/db/migrate.ts +125 -0
- package/templates/default/src/server/db/schema.ts +170 -0
- package/templates/default/src/server/db/seed.ts +130 -0
- package/templates/default/src/types/global.d.ts +25 -0
- package/templates/default/tailwind.config.js +46 -0
- package/templates/default/tsconfig.json +36 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance monitoring and query tracking module.
|
|
3
|
+
*
|
|
4
|
+
* This module provides tools to track database query performance and identify
|
|
5
|
+
* slow operations that may impact application responsiveness. It collects timing
|
|
6
|
+
* data for queries and provides methods to analyze performance patterns.
|
|
7
|
+
*
|
|
8
|
+
* Key patterns and conventions:
|
|
9
|
+
* - Singleton pattern: A single performanceMonitor instance is shared across
|
|
10
|
+
* the application to aggregate metrics
|
|
11
|
+
* - Query keys are truncated to 100 characters to reduce memory usage while
|
|
12
|
+
* preserving enough context to identify the query
|
|
13
|
+
* - Slow queries are logged to console in development mode only
|
|
14
|
+
* - Metrics are stored in-memory and lost on process restart (by design for simplicity)
|
|
15
|
+
* - Average duration is used instead of individual samples to identify patterns
|
|
16
|
+
*
|
|
17
|
+
* Performance implications:
|
|
18
|
+
* - Minimal overhead: timing measurements use performance.now() which is
|
|
19
|
+
* sub-microsecond precision and has negligible overhead
|
|
20
|
+
* - Memory usage grows with the number of unique queries but is bounded
|
|
21
|
+
* by query key truncation (100 chars per key)
|
|
22
|
+
* - No disk I/O: All metrics are kept in memory to avoid affecting query performance
|
|
23
|
+
* - Automatic collection: When wrapping queries with measureQuery/measureSyncQuery,
|
|
24
|
+
* timing happens automatically without manual instrumentation
|
|
25
|
+
*
|
|
26
|
+
* Configuration decisions:
|
|
27
|
+
* - SLOW_QUERY_THRESHOLD of 1000ms (1 second) is chosen because:
|
|
28
|
+
* - Web requests should ideally complete within 100ms-200ms
|
|
29
|
+
* - Database queries over 1 second are noticeably slow and worth investigating
|
|
30
|
+
* - Threshold is configurable via getSlowQueries(threshold) parameter
|
|
31
|
+
* - 100 character query key truncation balances:
|
|
32
|
+
* - Memory efficiency (shorter keys = less memory)
|
|
33
|
+
* - Debugging utility (enough context to identify the query)
|
|
34
|
+
* - Development-only logging avoids performance impact in production
|
|
35
|
+
* - No persistence: In-memory storage is sufficient for development debugging
|
|
36
|
+
*
|
|
37
|
+
* Usage patterns:
|
|
38
|
+
* - Wrap database queries with measureQuery for async operations
|
|
39
|
+
* - Wrap synchronous operations with measureSyncQuery
|
|
40
|
+
* - Call getSlowQueries() periodically to analyze performance
|
|
41
|
+
* - Call reset() to clear metrics (e.g., between test runs)
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Wrap an async database query
|
|
45
|
+
* const users = await measureQuery(
|
|
46
|
+
* 'SELECT * FROM users',
|
|
47
|
+
* () => db.query.users.findMany()
|
|
48
|
+
* )
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // Get slow queries for analysis
|
|
52
|
+
* const slowQueries = performanceMonitor.getSlowQueries()
|
|
53
|
+
* slowQueries.forEach(({ query, avgDuration, count }) => {
|
|
54
|
+
* console.log(`${query}: ${avgDuration}ms avg (${count} executions)`)
|
|
55
|
+
* })
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { env } from './env'
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Threshold in milliseconds for considering a query "slow".
|
|
62
|
+
*
|
|
63
|
+
* Any query taking longer than this duration will be logged in development
|
|
64
|
+
* and included in getSlowQueries() results.
|
|
65
|
+
*
|
|
66
|
+
* Rationale: 1000ms (1 second) is chosen because:
|
|
67
|
+
* - User-perceivable delay starts at around 100ms
|
|
68
|
+
* - Database queries over 1 second indicate potential optimization opportunities
|
|
69
|
+
* - Allows catching significant performance issues while filtering out
|
|
70
|
+
* minor variations that don't impact user experience
|
|
71
|
+
*/
|
|
72
|
+
const SLOW_QUERY_THRESHOLD = 1000 // 1 second in milliseconds
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Performance monitoring class for tracking query execution times.
|
|
76
|
+
*
|
|
77
|
+
* This class maintains a registry of query execution times and provides
|
|
78
|
+
* methods to record timings and analyze slow queries. Use the singleton
|
|
79
|
+
* instance `performanceMonitor` instead of instantiating this class directly.
|
|
80
|
+
*
|
|
81
|
+
* Thread safety: This class is not thread-safe but JavaScript is single-threaded,
|
|
82
|
+
* so this is not a concern in the Node.js runtime.
|
|
83
|
+
*
|
|
84
|
+
* Memory usage: Grows linearly with the number of unique queries. Each unique
|
|
85
|
+
* query stores an array of duration samples. For production use with many
|
|
86
|
+
* unique queries, consider implementing a periodic cleanup or sampling strategy.
|
|
87
|
+
*/
|
|
88
|
+
export class PerformanceMonitor {
|
|
89
|
+
/**
|
|
90
|
+
* Map storing query execution times.
|
|
91
|
+
*
|
|
92
|
+
* Key: First 100 characters of the query string
|
|
93
|
+
* Value: Array of execution durations in milliseconds
|
|
94
|
+
*
|
|
95
|
+
* Truncating keys to 100 characters reduces memory usage while preserving
|
|
96
|
+
* enough context to identify the query. Multiple variations of the same query
|
|
97
|
+
* (e.g., different WHERE clause values) will be aggregated under the same key,
|
|
98
|
+
* which is desirable for identifying patterns rather than individual executions.
|
|
99
|
+
*/
|
|
100
|
+
private queryTimes = new Map<string, number[]>()
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Records the execution time of a query.
|
|
104
|
+
*
|
|
105
|
+
* This method should be called immediately after a query completes with the
|
|
106
|
+
* actual duration measured. If the query exceeds the slow threshold, a warning
|
|
107
|
+
* will be logged in development mode.
|
|
108
|
+
*
|
|
109
|
+
Performance characteristics:
|
|
110
|
+
* - Time complexity: O(1) for Map insertion
|
|
111
|
+
* - Space complexity: O(n) where n is the number of unique queries
|
|
112
|
+
* - Negligible overhead (sub-microsecond for timing recording)
|
|
113
|
+
*
|
|
114
|
+
* Security implications:
|
|
115
|
+
* - Query strings may contain sensitive data (user inputs, etc.)
|
|
116
|
+
* - In production, avoid logging full queries if they contain PII
|
|
117
|
+
* - The truncated key (100 chars) reduces exposure risk
|
|
118
|
+
*
|
|
119
|
+
* @param query - The SQL query or operation description. Truncated to 100 chars.
|
|
120
|
+
* @param duration - Execution time in milliseconds. Should be measured using
|
|
121
|
+
* performance.now() for accuracy.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* const startTime = performance.now()
|
|
125
|
+
* await db.query.users.findMany()
|
|
126
|
+
* const duration = performance.now() - startTime
|
|
127
|
+
* performanceMonitor.recordQuery('SELECT * FROM users', duration)
|
|
128
|
+
*/
|
|
129
|
+
recordQuery(query: string, duration: number) {
|
|
130
|
+
/**
|
|
131
|
+
* Truncate query to first 100 characters.
|
|
132
|
+
*
|
|
133
|
+
* This serves two purposes:
|
|
134
|
+
* 1. Memory efficiency: shorter keys use less memory
|
|
135
|
+
* 2. Pattern aggregation: similar queries with different parameters
|
|
136
|
+
* are grouped together for analysis
|
|
137
|
+
*/
|
|
138
|
+
const queryKey = query.slice(0, 100)
|
|
139
|
+
|
|
140
|
+
if (!this.queryTimes.has(queryKey)) {
|
|
141
|
+
this.queryTimes.set(queryKey, [])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.queryTimes.get(queryKey)!.push(duration)
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Log slow queries in development mode.
|
|
148
|
+
*
|
|
149
|
+
* Development-only logging avoids:
|
|
150
|
+
* - Performance impact in production
|
|
151
|
+
* - Potential exposure of sensitive query data in production logs
|
|
152
|
+
* - Log noise in production where performance may be acceptable
|
|
153
|
+
*/
|
|
154
|
+
if (duration > SLOW_QUERY_THRESHOLD) {
|
|
155
|
+
if (env.NODE_ENV === 'development') {
|
|
156
|
+
console.warn(`[Slow Query] ${duration}ms: ${queryKey}`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Retrieves all queries that exceed a given average duration threshold.
|
|
163
|
+
*
|
|
164
|
+
* This method calculates the average execution time for each query and returns
|
|
165
|
+
* those whose average exceeds the threshold. Results are sorted by average
|
|
166
|
+
duration in descending order (slowest first) for prioritization.
|
|
167
|
+
*
|
|
168
|
+
* Performance characteristics:
|
|
169
|
+
* - Time complexity: O(n * m) where n is the number of unique queries and m
|
|
170
|
+
* is the average number of samples per query
|
|
171
|
+
* - Typically very fast unless thousands of queries have been recorded
|
|
172
|
+
* - Sorting adds O(n log n) overhead
|
|
173
|
+
*
|
|
174
|
+
* Use cases:
|
|
175
|
+
* - Identifying optimization opportunities during development
|
|
176
|
+
* - Generating performance reports
|
|
177
|
+
* - Debugging production performance issues (via logging)
|
|
178
|
+
*
|
|
179
|
+
* @param threshold - Average duration threshold in milliseconds. Defaults to
|
|
180
|
+
* SLOW_QUERY_THRESHOLD (1000ms). Queries with average
|
|
181
|
+
* duration below this are filtered out.
|
|
182
|
+
* @returns Array of slow query metrics, sorted by average duration descending.
|
|
183
|
+
* Each entry contains the query (truncated), average duration (rounded),
|
|
184
|
+
* and execution count.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const slowQueries = performanceMonitor.getSlowQueries(500)
|
|
188
|
+
* slowQueries.forEach(({ query, avgDuration, count }) => {
|
|
189
|
+
* console.log(`${query}: ${avgDuration}ms average over ${count} executions`)
|
|
190
|
+
* })
|
|
191
|
+
*/
|
|
192
|
+
getSlowQueries(
|
|
193
|
+
threshold = SLOW_QUERY_THRESHOLD
|
|
194
|
+
): Array<{ query: string; avgDuration: number; count: number }> {
|
|
195
|
+
const results: Array<{
|
|
196
|
+
query: string
|
|
197
|
+
avgDuration: number
|
|
198
|
+
count: number
|
|
199
|
+
}> = []
|
|
200
|
+
|
|
201
|
+
this.queryTimes.forEach((durations, query) => {
|
|
202
|
+
const avgDuration =
|
|
203
|
+
durations.reduce((a, b) => a + b, 0) / durations.length
|
|
204
|
+
|
|
205
|
+
if (avgDuration > threshold) {
|
|
206
|
+
results.push({
|
|
207
|
+
query,
|
|
208
|
+
avgDuration: Math.round(avgDuration),
|
|
209
|
+
count: durations.length,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Sort by average duration descending.
|
|
216
|
+
*
|
|
217
|
+
* This puts the slowest queries first, which is useful for prioritizing
|
|
218
|
+
* optimization efforts.
|
|
219
|
+
*/
|
|
220
|
+
return results.sort((a, b) => b.avgDuration - a.avgDuration)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Clears all recorded query metrics.
|
|
225
|
+
*
|
|
226
|
+
* This method is useful for:
|
|
227
|
+
* - Starting fresh measurements after code changes
|
|
228
|
+
* - Clearing metrics between test runs
|
|
229
|
+
* - Resetting state in development
|
|
230
|
+
*
|
|
231
|
+
* Performance characteristics:
|
|
232
|
+
* - Time complexity: O(n) where n is the number of unique queries
|
|
233
|
+
* - Memory is immediately released (in V8, subject to garbage collection)
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Clear metrics before running a performance test
|
|
237
|
+
* performanceMonitor.reset()
|
|
238
|
+
* await runPerformanceTest()
|
|
239
|
+
* const results = performanceMonitor.getSlowQueries()
|
|
240
|
+
*/
|
|
241
|
+
reset() {
|
|
242
|
+
this.queryTimes.clear()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Singleton instance of PerformanceMonitor.
|
|
248
|
+
*
|
|
249
|
+
* Use this instance throughout the application to aggregate metrics from
|
|
250
|
+
* all parts of the codebase. Having a single instance allows for comprehensive
|
|
251
|
+
* performance analysis across all database queries and operations.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* import { performanceMonitor } from '@/lib/monitoring'
|
|
255
|
+
* performanceMonitor.recordQuery('SELECT * FROM users', 125)
|
|
256
|
+
*/
|
|
257
|
+
export const performanceMonitor = new PerformanceMonitor()
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Wraps an async function with performance measurement.
|
|
261
|
+
*
|
|
262
|
+
* This utility automatically measures the execution time of an async function
|
|
263
|
+
* and records it with the performance monitor. Use this for database queries,
|
|
264
|
+
* API calls, or any async operation you want to track.
|
|
265
|
+
*
|
|
266
|
+
* Performance characteristics:
|
|
267
|
+
* - Overhead: ~0.1ms for timing (performance.now() calls)
|
|
268
|
+
* - Async/await overhead: minimal (<1ms) for Promise wrapping
|
|
269
|
+
* - Memory: No additional memory allocation per call
|
|
270
|
+
*
|
|
271
|
+
* Error handling: Errors are propagated unchanged. The timing measurement
|
|
272
|
+
* includes the time taken by the function, including any error handling.
|
|
273
|
+
*
|
|
274
|
+
* @param query - Description of the operation (e.g., SQL query or function name).
|
|
275
|
+
* Used as the key in the performance monitor.
|
|
276
|
+
* @param fn - The async function to measure. Should return a Promise.
|
|
277
|
+
* @returns Promise that resolves to the result of fn(), with timing recorded.
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* const users = await measureQuery(
|
|
281
|
+
* 'SELECT * FROM users WHERE active = true',
|
|
282
|
+
* () => db.query.users.findMany({ where: { active: true } })
|
|
283
|
+
* )
|
|
284
|
+
*/
|
|
285
|
+
export function measureQuery<T>(
|
|
286
|
+
query: string,
|
|
287
|
+
fn: () => Promise<T>
|
|
288
|
+
): Promise<T> {
|
|
289
|
+
return new Promise(async (resolve, reject) => {
|
|
290
|
+
const startTime = performance.now()
|
|
291
|
+
try {
|
|
292
|
+
const result = await fn()
|
|
293
|
+
const duration = performance.now() - startTime
|
|
294
|
+
performanceMonitor.recordQuery(query, duration)
|
|
295
|
+
resolve(result)
|
|
296
|
+
} catch (error) {
|
|
297
|
+
reject(error)
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Wraps a synchronous function with performance measurement.
|
|
304
|
+
*
|
|
305
|
+
* This utility automatically measures the execution time of a synchronous function
|
|
306
|
+
* and records it with the performance monitor. Use this for CPU-bound operations,
|
|
307
|
+
* synchronous I/O, or any synchronous code you want to track.
|
|
308
|
+
*
|
|
309
|
+
* Performance characteristics:
|
|
310
|
+
* - Overhead: ~0.1ms for timing (performance.now() calls)
|
|
311
|
+
* - Memory: No additional memory allocation per call
|
|
312
|
+
* - Suitable for operations taking >1ms (otherwise overhead is significant)
|
|
313
|
+
*
|
|
314
|
+
* Use cases:
|
|
315
|
+
* - Tracking synchronous database operations (e.g., SQLite queries)
|
|
316
|
+
* - Measuring data transformation time
|
|
317
|
+
* - Profiling CPU-intensive computations
|
|
318
|
+
*
|
|
319
|
+
* @param query - Description of the operation (e.g., function name or operation).
|
|
320
|
+
* Used as the key in the performance monitor.
|
|
321
|
+
* @param fn - The synchronous function to measure.
|
|
322
|
+
* @returns The result of fn(), with timing recorded.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* const users = measureSyncQuery(
|
|
326
|
+
* 'db.query.users.findMany()',
|
|
327
|
+
* () => db.query.users.findMany()
|
|
328
|
+
* )
|
|
329
|
+
*/
|
|
330
|
+
export function measureSyncQuery<T>(query: string, fn: () => T): T {
|
|
331
|
+
const startTime = performance.now()
|
|
332
|
+
const result = fn()
|
|
333
|
+
const duration = performance.now() - startTime
|
|
334
|
+
performanceMonitor.recordQuery(query, duration)
|
|
335
|
+
return result
|
|
336
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { router } from './trpc'
|
|
2
|
+
import { userRouter } from './routers/user'
|
|
3
|
+
import { dashboardRouter } from './routers/dashboard'
|
|
4
|
+
|
|
5
|
+
export const appRouter = router({
|
|
6
|
+
user: userRouter,
|
|
7
|
+
dashboard: dashboardRouter,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export type AppRouter = typeof appRouter
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { router, protectedProcedure } from '../trpc'
|
|
2
|
+
import { db } from '@/server/db'
|
|
3
|
+
import { users } from '@/server/db/schema'
|
|
4
|
+
import { eq, and, gte, sql } from 'drizzle-orm'
|
|
5
|
+
|
|
6
|
+
export const dashboardRouter = router({
|
|
7
|
+
getStats: protectedProcedure.query(async ({ ctx }) => {
|
|
8
|
+
const [totalUsersResult] = await db
|
|
9
|
+
.select({ count: sql<number>`count(*)` })
|
|
10
|
+
.from(users)
|
|
11
|
+
|
|
12
|
+
const [activeUsersResult] = await db
|
|
13
|
+
.select({ count: sql<number>`count(*)` })
|
|
14
|
+
.from(users)
|
|
15
|
+
.where(
|
|
16
|
+
and(
|
|
17
|
+
gte(users.createdAt, new Date(Date.now() - 30 * 24 * 60 * 60 * 1000))
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
totalUsers: Number(totalUsersResult?.count ?? 0),
|
|
23
|
+
activeUsers: Number(activeUsersResult?.count ?? 0),
|
|
24
|
+
currentUser: ctx.user,
|
|
25
|
+
}
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
getUserProfile: protectedProcedure.query(async ({ ctx }) => {
|
|
29
|
+
const [user] = await db
|
|
30
|
+
.select()
|
|
31
|
+
.from(users)
|
|
32
|
+
.where(eq(users.id, ctx.user.id))
|
|
33
|
+
.limit(1)
|
|
34
|
+
|
|
35
|
+
return user ?? null
|
|
36
|
+
}),
|
|
37
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { router, protectedProcedure } from '../trpc'
|
|
3
|
+
import { db } from '@/server/db'
|
|
4
|
+
import { users } from '@/server/db/schema'
|
|
5
|
+
import { eq } from 'drizzle-orm'
|
|
6
|
+
|
|
7
|
+
export const userRouter = router({
|
|
8
|
+
getProfile: protectedProcedure.query(async ({ ctx }) => {
|
|
9
|
+
const result = await (db
|
|
10
|
+
.select()
|
|
11
|
+
.from(users)
|
|
12
|
+
.where(eq(users.id, ctx.user.id))
|
|
13
|
+
.limit(1) as any)
|
|
14
|
+
return result[0] ?? null
|
|
15
|
+
}),
|
|
16
|
+
|
|
17
|
+
updateProfile: protectedProcedure
|
|
18
|
+
.input(
|
|
19
|
+
z.object({
|
|
20
|
+
name: z.string().min(1).max(100),
|
|
21
|
+
})
|
|
22
|
+
)
|
|
23
|
+
.mutation(async ({ ctx, input }) => {
|
|
24
|
+
const result = await (db
|
|
25
|
+
.update(users)
|
|
26
|
+
.set({ name: input.name, updatedAt: new Date() })
|
|
27
|
+
.where(eq(users.id, ctx.user.id))
|
|
28
|
+
.returning() as any)
|
|
29
|
+
return result[0]
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tRPC configuration and context setup
|
|
3
|
+
*
|
|
4
|
+
* This file sets up the tRPC server with:
|
|
5
|
+
* - Request context with database connection and authentication
|
|
6
|
+
* - Middleware for auth checks
|
|
7
|
+
* - Base procedures (public, protected) for different access levels
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Create a protected procedure
|
|
11
|
+
* export const userRouter = router({
|
|
12
|
+
* getProfile: protectedProcedure.query(({ ctx }) => {
|
|
13
|
+
* return ctx.user
|
|
14
|
+
* })
|
|
15
|
+
* })
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { initTRPC, TRPCError } from '@trpc/server'
|
|
19
|
+
import { auth } from '@/server/auth/config'
|
|
20
|
+
import { type FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates the tRPC context for each request
|
|
24
|
+
*
|
|
25
|
+
* This function is called for every tRPC request to create a context object
|
|
26
|
+
* that is available to all procedures. The context includes:
|
|
27
|
+
* - User session data from the authentication system
|
|
28
|
+
* - User information if authenticated, null otherwise
|
|
29
|
+
*
|
|
30
|
+
* Architectural decision: Context is created per-request rather than using
|
|
31
|
+
* a singleton because authentication state varies per request. Using async
|
|
32
|
+
* context creation allows us to fetch session data from cookies/headers.
|
|
33
|
+
*
|
|
34
|
+
* @param opts - Fetch adapter options containing the request object
|
|
35
|
+
* @param opts.req - The incoming HTTP request with headers
|
|
36
|
+
* @returns Context object with user and session data
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // The context is automatically used in procedures
|
|
40
|
+
* const procedure = publicProcedure.query(({ ctx }) => {
|
|
41
|
+
* console.log(ctx.user) // User object or null
|
|
42
|
+
* console.log(ctx.session) // Session object or null
|
|
43
|
+
* })
|
|
44
|
+
*/
|
|
45
|
+
export const createTRPCContext = async (opts: FetchCreateContextFnOptions) => {
|
|
46
|
+
const session = await auth.api.getSession({
|
|
47
|
+
headers: opts.req.headers,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
user: session?.user ?? null,
|
|
52
|
+
session: session?.session ?? null,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialized tRPC instance with context typing
|
|
58
|
+
*
|
|
59
|
+
* Architectural decision: We use initTRPC with .context() to create a
|
|
60
|
+
* strongly-typed tRPC instance. This ensures type safety across all routers
|
|
61
|
+
* and procedures, providing autocomplete and compile-time checks for the
|
|
62
|
+
* context object structure.
|
|
63
|
+
*/
|
|
64
|
+
const t = initTRPC.context<typeof createTRPCContext>().create()
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Router factory function
|
|
68
|
+
*
|
|
69
|
+
* Creates a new tRPC router by combining multiple procedures and sub-routers.
|
|
70
|
+
* Use this to organize API endpoints into logical groups.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* export const appRouter = router({
|
|
74
|
+
* greeting: publicProcedure.query(() => 'Hello World'),
|
|
75
|
+
* users: userRouter,
|
|
76
|
+
* })
|
|
77
|
+
*/
|
|
78
|
+
export const router = t.router
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Public procedure factory
|
|
82
|
+
*
|
|
83
|
+
* Creates a procedure that can be accessed without authentication.
|
|
84
|
+
* Use this for endpoints that don't require user login.
|
|
85
|
+
*
|
|
86
|
+
* Architectural decision: Having explicit public and protected procedures
|
|
87
|
+
* makes authentication requirements clear and prevents accidental exposure
|
|
88
|
+
* of protected data. The default is public to reduce friction for new endpoints.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* export const healthRouter = router({
|
|
92
|
+
* check: publicProcedure.query(() => ({ status: 'ok' }))
|
|
93
|
+
* })
|
|
94
|
+
*/
|
|
95
|
+
export const publicProcedure = t.procedure
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Protected procedure with authentication middleware
|
|
99
|
+
*
|
|
100
|
+
* Creates a procedure that requires user authentication. The middleware
|
|
101
|
+
* checks if a user is present in the context and throws an UNAUTHORIZED
|
|
102
|
+
* error if not.
|
|
103
|
+
*
|
|
104
|
+
* Architectural decision: This middleware pattern centralizes auth logic
|
|
105
|
+
* rather than checking authentication in each procedure. It also narrows
|
|
106
|
+
* the context type to guarantee that ctx.user and ctx.session are non-null,
|
|
107
|
+
* providing type safety in protected procedures.
|
|
108
|
+
*
|
|
109
|
+
* @throws {TRPCError} UNAUTHORIZED if user is not authenticated
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* export const userRouter = router({
|
|
113
|
+
* getProfile: protectedProcedure.query(({ ctx }) => {
|
|
114
|
+
* // ctx.user and ctx.session are guaranteed to be non-null here
|
|
115
|
+
* return ctx.user
|
|
116
|
+
* })
|
|
117
|
+
* })
|
|
118
|
+
*/
|
|
119
|
+
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
|
120
|
+
if (!ctx.user) {
|
|
121
|
+
throw new TRPCError({
|
|
122
|
+
code: 'UNAUTHORIZED',
|
|
123
|
+
message: 'You must be logged in to access this resource',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
return next({
|
|
127
|
+
ctx: {
|
|
128
|
+
user: ctx.user,
|
|
129
|
+
session: ctx.session!,
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
})
|