@odvi/create-dtt-framework 0.1.2 → 0.1.5

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 (114) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +16 -13
  3. package/dist/commands/create.js.map +1 -1
  4. package/dist/utils/template.d.ts.map +1 -1
  5. package/dist/utils/template.js +5 -0
  6. package/dist/utils/template.js.map +1 -1
  7. package/package.json +3 -2
  8. package/template/.env.example +103 -0
  9. package/template/components.json +22 -0
  10. package/template/docs/framework/01-overview.md +289 -0
  11. package/template/docs/framework/02-techstack.md +503 -0
  12. package/template/docs/framework/api-layer.md +681 -0
  13. package/template/docs/framework/clerk-authentication.md +649 -0
  14. package/template/docs/framework/cli-installation.md +564 -0
  15. package/template/docs/framework/deployment/ci-cd.md +907 -0
  16. package/template/docs/framework/deployment/digitalocean.md +991 -0
  17. package/template/docs/framework/deployment/domain-setup.md +972 -0
  18. package/template/docs/framework/deployment/environment-variables.md +863 -0
  19. package/template/docs/framework/deployment/monitoring.md +927 -0
  20. package/template/docs/framework/deployment/production-checklist.md +649 -0
  21. package/template/docs/framework/deployment/vercel.md +791 -0
  22. package/template/docs/framework/environment-variables.md +658 -0
  23. package/template/docs/framework/health-check-system.md +582 -0
  24. package/template/docs/framework/implementation.md +559 -0
  25. package/template/docs/framework/snowflake-integration.md +591 -0
  26. package/template/docs/framework/state-management.md +615 -0
  27. package/template/docs/framework/supabase-integration.md +581 -0
  28. package/template/docs/framework/testing-guide.md +544 -0
  29. package/template/docs/framework/what-did-i-miss.md +526 -0
  30. package/template/drizzle.config.ts +12 -0
  31. package/template/next.config.js +21 -0
  32. package/template/postcss.config.js +5 -0
  33. package/template/prettier.config.js +4 -0
  34. package/template/public/favicon.ico +0 -0
  35. package/template/src/app/(auth)/layout.tsx +4 -0
  36. package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
  37. package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
  38. package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
  39. package/template/src/app/(dashboard)/health/page.tsx +16 -0
  40. package/template/src/app/(dashboard)/layout.tsx +17 -0
  41. package/template/src/app/api/[[...route]]/route.ts +11 -0
  42. package/template/src/app/api/debug-files/route.ts +33 -0
  43. package/template/src/app/api/webhooks/clerk/route.ts +112 -0
  44. package/template/src/app/layout.tsx +28 -0
  45. package/template/src/app/page.tsx +12 -0
  46. package/template/src/app/providers.tsx +20 -0
  47. package/template/src/components/layouts/navbar.tsx +14 -0
  48. package/template/src/components/shared/loading-spinner.tsx +6 -0
  49. package/template/src/components/ui/badge.tsx +46 -0
  50. package/template/src/components/ui/button.tsx +62 -0
  51. package/template/src/components/ui/card.tsx +92 -0
  52. package/template/src/components/ui/collapsible.tsx +33 -0
  53. package/template/src/components/ui/scroll-area.tsx +58 -0
  54. package/template/src/components/ui/sheet.tsx +139 -0
  55. package/template/src/config/__tests__/env.test.ts +166 -0
  56. package/template/src/config/__tests__/site.test.ts +46 -0
  57. package/template/src/config/env.ts +36 -0
  58. package/template/src/config/site.ts +10 -0
  59. package/template/src/env.js +44 -0
  60. package/template/src/features/__tests__/health-check-config.test.ts +142 -0
  61. package/template/src/features/__tests__/health-check-types.test.ts +201 -0
  62. package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
  63. package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
  64. package/template/src/features/documentation/index.tsx +92 -0
  65. package/template/src/features/documentation/utils/doc-loader.ts +177 -0
  66. package/template/src/features/health-check/components/health-dashboard.tsx +363 -0
  67. package/template/src/features/health-check/config.ts +72 -0
  68. package/template/src/features/health-check/index.ts +4 -0
  69. package/template/src/features/health-check/stores/health-store.ts +14 -0
  70. package/template/src/features/health-check/types.ts +18 -0
  71. package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
  72. package/template/src/hooks/queries/use-health-checks.ts +16 -0
  73. package/template/src/hooks/utils/use-debounce.ts +20 -0
  74. package/template/src/lib/__tests__/utils.test.ts +52 -0
  75. package/template/src/lib/__tests__/validators.test.ts +114 -0
  76. package/template/src/lib/nextbank/client.ts +37 -0
  77. package/template/src/lib/snowflake/client.ts +53 -0
  78. package/template/src/lib/supabase/admin.ts +7 -0
  79. package/template/src/lib/supabase/client.ts +7 -0
  80. package/template/src/lib/supabase/server.ts +23 -0
  81. package/template/src/lib/utils.ts +6 -0
  82. package/template/src/lib/validators.ts +9 -0
  83. package/template/src/middleware.ts +22 -0
  84. package/template/src/server/api/index.ts +22 -0
  85. package/template/src/server/api/middleware/auth.ts +19 -0
  86. package/template/src/server/api/middleware/logger.ts +4 -0
  87. package/template/src/server/api/routes/health/clerk.ts +214 -0
  88. package/template/src/server/api/routes/health/database.ts +117 -0
  89. package/template/src/server/api/routes/health/edge-functions.ts +75 -0
  90. package/template/src/server/api/routes/health/framework.ts +45 -0
  91. package/template/src/server/api/routes/health/index.ts +102 -0
  92. package/template/src/server/api/routes/health/nextbank.ts +67 -0
  93. package/template/src/server/api/routes/health/snowflake.ts +83 -0
  94. package/template/src/server/api/routes/health/storage.ts +163 -0
  95. package/template/src/server/api/routes/users.ts +95 -0
  96. package/template/src/server/db/index.ts +17 -0
  97. package/template/src/server/db/queries/users.ts +8 -0
  98. package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
  99. package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
  100. package/template/src/server/db/schema/health-checks.ts +11 -0
  101. package/template/src/server/db/schema/index.ts +2 -0
  102. package/template/src/server/db/schema/users.ts +16 -0
  103. package/template/src/server/db/schema.ts +26 -0
  104. package/template/src/stores/__tests__/ui-store.test.ts +87 -0
  105. package/template/src/stores/ui-store.ts +14 -0
  106. package/template/src/styles/globals.css +129 -0
  107. package/template/src/test/mocks/clerk.ts +35 -0
  108. package/template/src/test/mocks/snowflake.ts +28 -0
  109. package/template/src/test/mocks/supabase.ts +37 -0
  110. package/template/src/test/setup.ts +69 -0
  111. package/template/src/test/utils/test-helpers.ts +158 -0
  112. package/template/src/types/index.ts +14 -0
  113. package/template/tsconfig.json +43 -0
  114. package/template/vitest.config.ts +44 -0
@@ -0,0 +1,75 @@
1
+ import { Hono } from 'hono'
2
+ import { supabaseAdmin } from '@/lib/supabase/admin'
3
+ import { env } from '@/config/env'
4
+
5
+ export const edgeFunctionsHealthRoutes = new Hono()
6
+
7
+ edgeFunctionsHealthRoutes.get('/ping', async (c) => {
8
+ const start = performance.now()
9
+
10
+ try {
11
+ // Note: This is a placeholder implementation
12
+ // In a real setup, you would have an actual edge function deployed
13
+ // and would invoke it using Supabase's invoke function
14
+
15
+ // For now, we'll simulate a successful ping by checking the Supabase connection
16
+ const { data, error } = await supabaseAdmin
17
+ .from('_test_connection_')
18
+ .select('*')
19
+ .limit(1)
20
+
21
+ // Even if the table doesn't exist, if we get a connection error, we know Supabase is reachable
22
+ // If we get a different error, it means the connection worked
23
+
24
+ return c.json({
25
+ status: 'healthy',
26
+ responseTimeMs: Math.round(performance.now() - start),
27
+ message: 'Successfully pinged Supabase (edge functions placeholder)',
28
+ data: {
29
+ note: 'This is a placeholder - deploy an actual edge function to test real functionality',
30
+ supabaseUrl: env.NEXT_PUBLIC_SUPABASE_URL,
31
+ },
32
+ })
33
+ } catch (error) {
34
+ return c.json(
35
+ {
36
+ status: 'error',
37
+ responseTimeMs: Math.round(performance.now() - start),
38
+ error: error instanceof Error ? error.message : 'Failed to ping edge function',
39
+ },
40
+ 500
41
+ )
42
+ }
43
+ })
44
+
45
+ edgeFunctionsHealthRoutes.get('/auth', async (c) => {
46
+ const start = performance.now()
47
+
48
+ try {
49
+ // Note: This is a placeholder implementation
50
+ // In a real setup, you would invoke an edge function with an auth header
51
+ // and verify that the function can access and validate the auth token
52
+
53
+ // For now, we'll verify that we can create a valid auth header
54
+ const authHeader = c.req.header('authorization')
55
+
56
+ return c.json({
57
+ status: 'healthy',
58
+ responseTimeMs: Math.round(performance.now() - start),
59
+ message: 'Successfully verified auth header capability (edge functions placeholder)',
60
+ data: {
61
+ hasAuthHeader: !!authHeader,
62
+ note: 'This is a placeholder - deploy an actual edge function to test real functionality',
63
+ },
64
+ })
65
+ } catch (error) {
66
+ return c.json(
67
+ {
68
+ status: 'error',
69
+ responseTimeMs: Math.round(performance.now() - start),
70
+ error: error instanceof Error ? error.message : 'Failed to test auth header',
71
+ },
72
+ 500
73
+ )
74
+ }
75
+ })
@@ -0,0 +1,45 @@
1
+ import { Hono } from 'hono'
2
+ import { db } from '@/server/db'
3
+ import { sql } from 'drizzle-orm'
4
+
5
+ export const frameworkHealthRoutes = new Hono()
6
+
7
+ frameworkHealthRoutes.get('/hono', (c) => {
8
+ const start = performance.now()
9
+ return c.json({
10
+ status: 'healthy',
11
+ responseTimeMs: Math.round(performance.now() - start),
12
+ message: 'Hono framework is running correctly',
13
+ data: {
14
+ version: '4.11.3', // matching package.json
15
+ environment: process.env.NODE_ENV,
16
+ },
17
+ })
18
+ })
19
+
20
+ frameworkHealthRoutes.get('/drizzle', async (c) => {
21
+ const start = performance.now()
22
+ try {
23
+ // Simple SELECT 1 to verify ORM connection
24
+ await db.execute(sql`SELECT 1`)
25
+
26
+ return c.json({
27
+ status: 'healthy',
28
+ responseTimeMs: Math.round(performance.now() - start),
29
+ message: 'Drizzle ORM is connected and executing queries',
30
+ data: {
31
+ dialect: 'postgres',
32
+ },
33
+ })
34
+ } catch (error) {
35
+ return c.json(
36
+ {
37
+ status: 'error',
38
+ responseTimeMs: Math.round(performance.now() - start),
39
+ error: error instanceof Error ? error.message : 'Drizzle ORM check failed',
40
+ },
41
+ 500
42
+ )
43
+ }
44
+ })
45
+
@@ -0,0 +1,102 @@
1
+ import { Hono } from 'hono'
2
+ import { clerkHealthRoutes } from './clerk'
3
+ import { databaseHealthRoutes } from './database'
4
+ import { storageHealthRoutes } from './storage'
5
+ import { edgeFunctionsHealthRoutes } from './edge-functions'
6
+ import { snowflakeHealthRoutes } from './snowflake'
7
+ import { nextbankHealthRoutes } from './nextbank'
8
+ import { frameworkHealthRoutes } from './framework'
9
+
10
+ export const healthRoutes = new Hono()
11
+
12
+ healthRoutes.route('/clerk', clerkHealthRoutes)
13
+ healthRoutes.route('/database', databaseHealthRoutes)
14
+ healthRoutes.route('/storage', storageHealthRoutes)
15
+ healthRoutes.route('/edge', edgeFunctionsHealthRoutes)
16
+ healthRoutes.route('/snowflake', snowflakeHealthRoutes)
17
+ healthRoutes.route('/nextbank', nextbankHealthRoutes)
18
+ healthRoutes.route('/framework', frameworkHealthRoutes)
19
+
20
+ healthRoutes.get('/all', async (c) => {
21
+ const start = performance.now()
22
+ const baseUrl = new URL(c.req.url).origin
23
+
24
+ // Define all health check endpoints to call
25
+ const checks = [
26
+ { name: 'Clerk User', url: `${baseUrl}/api/health/clerk/user` },
27
+ { name: 'Clerk Org', url: `${baseUrl}/api/health/clerk/org` },
28
+ { name: 'Clerk Members', url: `${baseUrl}/api/health/clerk/members` },
29
+ { name: 'Database Write', url: `${baseUrl}/api/health/database/write`, method: 'POST' as const },
30
+ { name: 'Database Read', url: `${baseUrl}/api/health/database/read` },
31
+ { name: 'Database Delete', url: `${baseUrl}/api/health/database/delete`, method: 'DELETE' as const },
32
+ { name: 'Storage Upload', url: `${baseUrl}/api/health/storage/upload`, method: 'POST' as const },
33
+ { name: 'Storage Download', url: `${baseUrl}/api/health/storage/download` },
34
+ { name: 'Storage Delete', url: `${baseUrl}/api/health/storage/delete`, method: 'DELETE' as const },
35
+ { name: 'Edge Ping', url: `${baseUrl}/api/health/edge/ping` },
36
+ { name: 'Edge Auth', url: `${baseUrl}/api/health/edge/auth` },
37
+ { name: 'Snowflake Connect', url: `${baseUrl}/api/health/snowflake/connect` },
38
+ { name: 'Snowflake Query', url: `${baseUrl}/api/health/snowflake/query` },
39
+ { name: 'NextBank Ping', url: `${baseUrl}/api/health/nextbank/ping` },
40
+ { name: 'NextBank Auth', url: `${baseUrl}/api/health/nextbank/auth` },
41
+ ]
42
+
43
+ // Run all checks in parallel
44
+ const results = await Promise.allSettled(
45
+ checks.map(async (check) => {
46
+ const checkStart = performance.now()
47
+ try {
48
+ const response = await fetch(check.url, {
49
+ method: check.method || 'GET',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ // Forward auth header if present
53
+ ...(c.req.header('authorization') && {
54
+ authorization: c.req.header('authorization')!,
55
+ }),
56
+ },
57
+ })
58
+
59
+ const data = await response.json()
60
+
61
+ return {
62
+ name: check.name,
63
+ status: response.ok ? data.status || 'healthy' : 'error',
64
+ responseTimeMs: Math.round(performance.now() - checkStart),
65
+ httpStatus: response.status,
66
+ data,
67
+ }
68
+ } catch (error) {
69
+ return {
70
+ name: check.name,
71
+ status: 'error',
72
+ responseTimeMs: Math.round(performance.now() - checkStart),
73
+ httpStatus: 500,
74
+ error: error instanceof Error ? error.message : 'Check failed',
75
+ }
76
+ }
77
+ })
78
+ )
79
+
80
+ // Aggregate results by service
81
+ const services = {
82
+ clerk: results.filter((r) => r.status === 'fulfilled' && r.value.name.includes('Clerk')),
83
+ database: results.filter((r) => r.status === 'fulfilled' && r.value.name.includes('Database')),
84
+ storage: results.filter((r) => r.status === 'fulfilled' && r.value.name.includes('Storage')),
85
+ edge: results.filter((r) => r.status === 'fulfilled' && r.value.name.includes('Edge')),
86
+ snowflake: results.filter((r) => r.status === 'fulfilled' && r.value.name.includes('Snowflake')),
87
+ nextbank: results.filter((r) => r.status === 'fulfilled' && r.value.name.includes('NextBank')),
88
+ }
89
+
90
+ // Determine overall status
91
+ const allResults = results.map((r) => (r.status === 'fulfilled' ? r.value : null))
92
+ const hasErrors = allResults.some((r) => r && (r.status === 'error' || r.httpStatus >= 400))
93
+ const allHealthy = allResults.every((r) => r && r.status === 'healthy')
94
+
95
+ return c.json({
96
+ status: hasErrors ? 'unhealthy' : allHealthy ? 'healthy' : 'partial',
97
+ responseTimeMs: Math.round(performance.now() - start),
98
+ timestamp: new Date().toISOString(),
99
+ services: services,
100
+ checks: allResults,
101
+ })
102
+ })
@@ -0,0 +1,67 @@
1
+ import { Hono } from 'hono'
2
+ import { nextbankClient } from '@/lib/nextbank/client'
3
+ import { env } from '@/config/env'
4
+
5
+ export const nextbankHealthRoutes = new Hono()
6
+
7
+ nextbankHealthRoutes.get('/ping', async (c) => {
8
+ const start = performance.now()
9
+
10
+ if (!env.NEXTBANK_API_URL || env.NEXTBANK_API_URL === 'placeholder') {
11
+ return c.json({
12
+ status: 'unconfigured',
13
+ responseTimeMs: Math.round(performance.now() - start),
14
+ message: 'NextBank API not configured',
15
+ })
16
+ }
17
+
18
+ try {
19
+ const result = await nextbankClient.ping()
20
+ return c.json({
21
+ status: 'healthy',
22
+ responseTimeMs: Math.round(performance.now() - start),
23
+ message: 'Successfully pinged NextBank API',
24
+ data: result,
25
+ })
26
+ } catch (error) {
27
+ return c.json(
28
+ {
29
+ status: 'error',
30
+ responseTimeMs: Math.round(performance.now() - start),
31
+ error: error instanceof Error ? error.message : 'Ping failed',
32
+ },
33
+ 500
34
+ )
35
+ }
36
+ })
37
+
38
+ nextbankHealthRoutes.get('/auth', async (c) => {
39
+ const start = performance.now()
40
+
41
+ if (!env.NEXTBANK_CLIENT_ID || env.NEXTBANK_CLIENT_ID === 'placeholder') {
42
+ return c.json({
43
+ status: 'unconfigured',
44
+ responseTimeMs: Math.round(performance.now() - start),
45
+ message: 'NextBank OAuth not configured',
46
+ })
47
+ }
48
+
49
+ try {
50
+ const result = await nextbankClient.authenticate()
51
+ return c.json({
52
+ status: 'healthy',
53
+ responseTimeMs: Math.round(performance.now() - start),
54
+ message: 'Successfully authenticated with NextBank',
55
+ data: result,
56
+ })
57
+ } catch (error) {
58
+ return c.json(
59
+ {
60
+ status: 'error',
61
+ responseTimeMs: Math.round(performance.now() - start),
62
+ error: error instanceof Error ? error.message : 'Auth failed',
63
+ },
64
+ 500
65
+ )
66
+ }
67
+ })
@@ -0,0 +1,83 @@
1
+ import { Hono } from 'hono'
2
+ import { connectSnowflake, executeQuery, destroyConnection } from '@/lib/snowflake/client'
3
+ import { env } from '@/config/env'
4
+
5
+ export const snowflakeHealthRoutes = new Hono()
6
+
7
+ snowflakeHealthRoutes.get('/connect', async (c) => {
8
+ const start = performance.now()
9
+
10
+ if (!env.SNOWFLAKE_ACCOUNT || env.SNOWFLAKE_ACCOUNT === 'placeholder') {
11
+ return c.json({
12
+ status: 'unconfigured',
13
+ responseTimeMs: Math.round(performance.now() - start),
14
+ message: 'Snowflake not configured',
15
+ })
16
+ }
17
+
18
+ try {
19
+ const connection = await connectSnowflake()
20
+ await destroyConnection(connection)
21
+
22
+ return c.json({
23
+ status: 'healthy',
24
+ responseTimeMs: Math.round(performance.now() - start),
25
+ message: 'Successfully connected to Snowflake',
26
+ data: {
27
+ account: env.SNOWFLAKE_ACCOUNT,
28
+ warehouse: env.SNOWFLAKE_WAREHOUSE,
29
+ database: env.SNOWFLAKE_DATABASE,
30
+ schema: env.SNOWFLAKE_SCHEMA,
31
+ },
32
+ })
33
+ } catch (error) {
34
+ return c.json(
35
+ {
36
+ status: 'error',
37
+ responseTimeMs: Math.round(performance.now() - start),
38
+ error: error instanceof Error ? error.message : 'Connection failed',
39
+ },
40
+ 500
41
+ )
42
+ }
43
+ })
44
+
45
+ snowflakeHealthRoutes.get('/query', async (c) => {
46
+ const start = performance.now()
47
+
48
+ if (!env.SNOWFLAKE_ACCOUNT || env.SNOWFLAKE_ACCOUNT === 'placeholder') {
49
+ return c.json({
50
+ status: 'unconfigured',
51
+ responseTimeMs: Math.round(performance.now() - start),
52
+ message: 'Snowflake not configured',
53
+ })
54
+ }
55
+
56
+ try {
57
+ const connection = await connectSnowflake()
58
+ const rows = await executeQuery<{ CURRENT_TIMESTAMP: string }>(
59
+ connection,
60
+ 'SELECT CURRENT_TIMESTAMP() as CURRENT_TIMESTAMP'
61
+ )
62
+ await destroyConnection(connection)
63
+
64
+ return c.json({
65
+ status: 'healthy',
66
+ responseTimeMs: Math.round(performance.now() - start),
67
+ message: 'Query executed successfully',
68
+ data: {
69
+ timestamp: rows[0]?.CURRENT_TIMESTAMP,
70
+ rowCount: rows.length,
71
+ },
72
+ })
73
+ } catch (error) {
74
+ return c.json(
75
+ {
76
+ status: 'error',
77
+ responseTimeMs: Math.round(performance.now() - start),
78
+ error: error instanceof Error ? error.message : 'Query failed',
79
+ },
80
+ 500
81
+ )
82
+ }
83
+ })
@@ -0,0 +1,163 @@
1
+ import { Hono } from 'hono'
2
+ import { supabaseAdmin } from '@/lib/supabase/admin'
3
+ import { env } from '@/config/env'
4
+
5
+ const BUCKET_NAME = 'health-check-bucket'
6
+ const TEST_FILE_PATH = 'health-check/test-file.txt'
7
+
8
+ export const storageHealthRoutes = new Hono()
9
+
10
+ storageHealthRoutes.post('/upload', async (c) => {
11
+ const start = performance.now()
12
+
13
+ try {
14
+ // Ensure bucket exists
15
+ const { data: buckets } = await supabaseAdmin.storage.listBuckets()
16
+ const bucketExists = buckets?.some((b) => b.name === BUCKET_NAME)
17
+
18
+ if (!bucketExists) {
19
+ // Create bucket if it doesn't exist
20
+ const { error: createError } = await supabaseAdmin.storage.createBucket(
21
+ BUCKET_NAME,
22
+ {
23
+ public: false,
24
+ }
25
+ )
26
+ if (createError) throw createError
27
+ }
28
+
29
+ // Upload test file
30
+ const testContent = `Health check test file - ${new Date().toISOString()}`
31
+ const { data, error } = await supabaseAdmin.storage
32
+ .from(BUCKET_NAME)
33
+ .upload(TEST_FILE_PATH, new Blob([testContent], { type: 'text/plain' }), {
34
+ upsert: true,
35
+ })
36
+
37
+ if (error) throw error
38
+
39
+ return c.json({
40
+ status: 'healthy',
41
+ responseTimeMs: Math.round(performance.now() - start),
42
+ message: 'Successfully uploaded test file to storage',
43
+ data: {
44
+ path: data?.path,
45
+ bucket: BUCKET_NAME,
46
+ },
47
+ })
48
+ } catch (error) {
49
+ return c.json(
50
+ {
51
+ status: 'error',
52
+ responseTimeMs: Math.round(performance.now() - start),
53
+ error: error instanceof Error ? error.message : 'Failed to upload to storage',
54
+ },
55
+ 500
56
+ )
57
+ }
58
+ })
59
+
60
+ storageHealthRoutes.get('/download', async (c) => {
61
+ const start = performance.now()
62
+
63
+ try {
64
+ // Check if file exists
65
+ const { data: fileData, error: checkError } = await supabaseAdmin.storage
66
+ .from(BUCKET_NAME)
67
+ .list('health-check', {
68
+ limit: 10,
69
+ })
70
+
71
+ if (checkError) throw checkError
72
+
73
+ const testFile = fileData?.find((f) => f.name === 'test-file.txt')
74
+
75
+ if (!testFile) {
76
+ return c.json({
77
+ status: 'healthy',
78
+ responseTimeMs: Math.round(performance.now() - start),
79
+ message: 'Storage connection successful, but no test file found',
80
+ data: { found: false },
81
+ })
82
+ }
83
+
84
+ // Get signed URL for download
85
+ const { data, error } = await supabaseAdmin.storage
86
+ .from(BUCKET_NAME)
87
+ .createSignedUrl(TEST_FILE_PATH, 60)
88
+
89
+ if (error) throw error
90
+
91
+ return c.json({
92
+ status: 'healthy',
93
+ responseTimeMs: Math.round(performance.now() - start),
94
+ message: 'Successfully generated download URL for test file',
95
+ data: {
96
+ found: true,
97
+ signedUrl: data?.signedUrl,
98
+ path: TEST_FILE_PATH,
99
+ },
100
+ })
101
+ } catch (error) {
102
+ return c.json(
103
+ {
104
+ status: 'error',
105
+ responseTimeMs: Math.round(performance.now() - start),
106
+ error: error instanceof Error ? error.message : 'Failed to download from storage',
107
+ },
108
+ 500
109
+ )
110
+ }
111
+ })
112
+
113
+ storageHealthRoutes.delete('/delete', async (c) => {
114
+ const start = performance.now()
115
+
116
+ try {
117
+ // Check if file exists first
118
+ const { data: fileData, error: checkError } = await supabaseAdmin.storage
119
+ .from(BUCKET_NAME)
120
+ .list('health-check', {
121
+ limit: 10,
122
+ })
123
+
124
+ if (checkError) throw checkError
125
+
126
+ const testFile = fileData?.find((f) => f.name === 'test-file.txt')
127
+
128
+ if (!testFile) {
129
+ return c.json({
130
+ status: 'healthy',
131
+ responseTimeMs: Math.round(performance.now() - start),
132
+ message: 'Storage connection successful, but no test file to delete',
133
+ data: { deleted: false },
134
+ })
135
+ }
136
+
137
+ // Delete test file
138
+ const { error } = await supabaseAdmin.storage
139
+ .from(BUCKET_NAME)
140
+ .remove([TEST_FILE_PATH])
141
+
142
+ if (error) throw error
143
+
144
+ return c.json({
145
+ status: 'healthy',
146
+ responseTimeMs: Math.round(performance.now() - start),
147
+ message: 'Successfully deleted test file from storage',
148
+ data: {
149
+ deleted: true,
150
+ path: TEST_FILE_PATH,
151
+ },
152
+ })
153
+ } catch (error) {
154
+ return c.json(
155
+ {
156
+ status: 'error',
157
+ responseTimeMs: Math.round(performance.now() - start),
158
+ error: error instanceof Error ? error.message : 'Failed to delete from storage',
159
+ },
160
+ 500
161
+ )
162
+ }
163
+ })
@@ -0,0 +1,95 @@
1
+ import { Hono } from 'hono'
2
+ import { db } from '@/server/db'
3
+ import { users } from '@/server/db/schema/users'
4
+ import { eq, desc } from 'drizzle-orm'
5
+
6
+ export const usersRoutes = new Hono()
7
+
8
+ usersRoutes.get('/', async (c) => {
9
+ const start = performance.now()
10
+
11
+ try {
12
+ const allUsers = await db
13
+ .select({
14
+ id: users.id,
15
+ email: users.email,
16
+ firstName: users.firstName,
17
+ lastName: users.lastName,
18
+ imageUrl: users.imageUrl,
19
+ clerkOrgId: users.clerkOrgId,
20
+ createdAt: users.createdAt,
21
+ updatedAt: users.updatedAt,
22
+ })
23
+ .from(users)
24
+ .orderBy(desc(users.createdAt))
25
+ .limit(100)
26
+
27
+ return c.json({
28
+ status: 'healthy',
29
+ responseTimeMs: Math.round(performance.now() - start),
30
+ message: 'Successfully retrieved users',
31
+ data: {
32
+ users: allUsers,
33
+ count: allUsers.length,
34
+ },
35
+ })
36
+ } catch (error) {
37
+ return c.json(
38
+ {
39
+ status: 'error',
40
+ responseTimeMs: Math.round(performance.now() - start),
41
+ error: error instanceof Error ? error.message : 'Failed to retrieve users',
42
+ },
43
+ 500
44
+ )
45
+ }
46
+ })
47
+
48
+ usersRoutes.get('/:id', async (c) => {
49
+ const start = performance.now()
50
+ const id = c.req.param('id')
51
+
52
+ try {
53
+ const user = await db
54
+ .select({
55
+ id: users.id,
56
+ email: users.email,
57
+ firstName: users.firstName,
58
+ lastName: users.lastName,
59
+ imageUrl: users.imageUrl,
60
+ clerkOrgId: users.clerkOrgId,
61
+ createdAt: users.createdAt,
62
+ updatedAt: users.updatedAt,
63
+ })
64
+ .from(users)
65
+ .where(eq(users.id, id))
66
+ .limit(1)
67
+
68
+ if (user.length === 0) {
69
+ return c.json(
70
+ {
71
+ status: 'error',
72
+ responseTimeMs: Math.round(performance.now() - start),
73
+ error: 'User not found',
74
+ },
75
+ 404
76
+ )
77
+ }
78
+
79
+ return c.json({
80
+ status: 'healthy',
81
+ responseTimeMs: Math.round(performance.now() - start),
82
+ message: 'Successfully retrieved user',
83
+ data: { user: user[0] },
84
+ })
85
+ } catch (error) {
86
+ return c.json(
87
+ {
88
+ status: 'error',
89
+ responseTimeMs: Math.round(performance.now() - start),
90
+ error: error instanceof Error ? error.message : 'Failed to retrieve user',
91
+ },
92
+ 500
93
+ )
94
+ }
95
+ })
@@ -0,0 +1,17 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js'
2
+ import postgres from 'postgres'
3
+ import { env } from '@/config/env'
4
+ import * as schema from './schema'
5
+
6
+ /**
7
+ * Cache the database connection in development. This avoids creating a new connection on every HMR
8
+ * update.
9
+ */
10
+ const globalForDb = globalThis as unknown as {
11
+ conn: postgres.Sql | undefined
12
+ }
13
+
14
+ const conn = globalForDb.conn ?? postgres(env.DATABASE_URL, { prepare: false })
15
+ if (env.NODE_ENV !== 'production') globalForDb.conn = conn
16
+
17
+ export const db = drizzle(conn, { schema })
@@ -0,0 +1,8 @@
1
+ import { db } from '../index'
2
+ import { users } from '../schema/users'
3
+ import { eq } from 'drizzle-orm'
4
+
5
+ export async function getUserById(id: string) {
6
+ const user = await db.select().from(users).where(eq(users.id, id)).limit(1)
7
+ return user[0] ?? null
8
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { healthCheckTests, type HealthCheckTest } from '../health-checks';
3
+
4
+ describe('healthCheckTests schema', () => {
5
+ it('should have healthCheckTests table defined', () => {
6
+ expect(healthCheckTests).toBeDefined();
7
+ });
8
+
9
+ it('should export HealthCheckTest type', () => {
10
+ const healthCheckTest: HealthCheckTest = {
11
+ id: 'test-uuid',
12
+ testKey: 'test-key',
13
+ testValue: 'test-value',
14
+ createdAt: new Date(),
15
+ };
16
+ expect(healthCheckTest).toBeDefined();
17
+ expect(healthCheckTest.testKey).toBe('test-key');
18
+ });
19
+
20
+ it('should allow HealthCheckTest with null testValue', () => {
21
+ const healthCheckTest: HealthCheckTest = {
22
+ id: 'test-uuid',
23
+ testKey: 'test-key',
24
+ testValue: null,
25
+ createdAt: new Date(),
26
+ };
27
+ expect(healthCheckTest).toBeDefined();
28
+ expect(healthCheckTest.testKey).toBe('test-key');
29
+ expect(healthCheckTest.testValue).toBeNull();
30
+ });
31
+ });