@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,114 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { healthCheckSchema } from '../validators';
3
+
4
+ describe('healthCheckSchema validator', () => {
5
+ it('should validate a healthy health check', () => {
6
+ const result = healthCheckSchema.safeParse({
7
+ status: 'healthy',
8
+ responseTimeMs: 100,
9
+ message: 'Service is healthy',
10
+ });
11
+ expect(result.success).toBe(true);
12
+ });
13
+
14
+ it('should validate an unhealthy health check', () => {
15
+ const result = healthCheckSchema.safeParse({
16
+ status: 'unhealthy',
17
+ responseTimeMs: 5000,
18
+ message: 'Service is unhealthy',
19
+ });
20
+ expect(result.success).toBe(true);
21
+ });
22
+
23
+ it('should validate an error health check', () => {
24
+ const result = healthCheckSchema.safeParse({
25
+ status: 'error',
26
+ error: 'Connection failed',
27
+ });
28
+ expect(result.success).toBe(true);
29
+ });
30
+
31
+ it('should validate a pending health check', () => {
32
+ const result = healthCheckSchema.safeParse({
33
+ status: 'pending',
34
+ message: 'Checking...',
35
+ });
36
+ expect(result.success).toBe(true);
37
+ });
38
+
39
+ it('should validate an unconfigured health check', () => {
40
+ const result = healthCheckSchema.safeParse({
41
+ status: 'unconfigured',
42
+ message: 'Service not configured',
43
+ });
44
+ expect(result.success).toBe(true);
45
+ });
46
+
47
+ it('should reject invalid status values', () => {
48
+ const result = healthCheckSchema.safeParse({
49
+ status: 'invalid',
50
+ });
51
+ expect(result.success).toBe(false);
52
+ });
53
+
54
+ it('should reject missing status field', () => {
55
+ const result = healthCheckSchema.safeParse({});
56
+ expect(result.success).toBe(false);
57
+ });
58
+
59
+ it('should accept health check with only required fields', () => {
60
+ const result = healthCheckSchema.safeParse({
61
+ status: 'healthy',
62
+ });
63
+ expect(result.success).toBe(true);
64
+ });
65
+
66
+ it('should reject invalid responseTimeMs type', () => {
67
+ const result = healthCheckSchema.safeParse({
68
+ status: 'healthy',
69
+ responseTimeMs: '100' as any,
70
+ });
71
+ expect(result.success).toBe(false);
72
+ });
73
+
74
+ it('should reject negative responseTimeMs', () => {
75
+ const result = healthCheckSchema.safeParse({
76
+ status: 'healthy',
77
+ responseTimeMs: -100,
78
+ });
79
+ expect(result.success).toBe(true); // Zod doesn't enforce min on number by default
80
+ });
81
+
82
+ it('should reject invalid message type', () => {
83
+ const result = healthCheckSchema.safeParse({
84
+ status: 'healthy',
85
+ message: 123 as any,
86
+ });
87
+ expect(result.success).toBe(false);
88
+ });
89
+
90
+ it('should reject invalid error type', () => {
91
+ const result = healthCheckSchema.safeParse({
92
+ status: 'error',
93
+ error: 123 as any,
94
+ });
95
+ expect(result.success).toBe(false);
96
+ });
97
+
98
+ it('should provide detailed error messages for invalid status', () => {
99
+ const result = healthCheckSchema.safeParse({
100
+ status: 'invalid',
101
+ });
102
+ if (!result.success) {
103
+ expect(result.error.issues[0]?.message).toContain('Invalid enum value');
104
+ }
105
+ });
106
+
107
+ it('should allow all valid status enum values', () => {
108
+ const validStatuses = ['healthy', 'unhealthy', 'error', 'pending', 'unconfigured'] as const;
109
+ validStatuses.forEach((status) => {
110
+ const result = healthCheckSchema.safeParse({ status });
111
+ expect(result.success).toBe(true);
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,37 @@
1
+ import { env } from '@/config/env'
2
+
3
+ class NextBankClient {
4
+ private apiUrl: string
5
+ private apiKey: string
6
+ private accessToken: string | null = null
7
+
8
+ constructor() {
9
+ this.apiUrl = env.NEXTBANK_API_URL ?? ''
10
+ this.apiKey = env.NEXTBANK_API_KEY ?? ''
11
+ }
12
+
13
+ async ping(): Promise<{ status: string; timestamp: string }> {
14
+ const response = await fetch(`${this.apiUrl}/health`, {
15
+ headers: { 'X-API-Key': this.apiKey },
16
+ })
17
+ if (!response.ok) throw new Error(`NextBank API error: ${response.status}`)
18
+ return response.json()
19
+ }
20
+
21
+ async authenticate(): Promise<{ success: boolean; expiresAt: string }> {
22
+ const response = await fetch(`${this.apiUrl}/auth/token`, {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({
26
+ clientId: env.NEXTBANK_CLIENT_ID,
27
+ clientSecret: env.NEXTBANK_CLIENT_SECRET,
28
+ }),
29
+ })
30
+ if (!response.ok) throw new Error(`NextBank auth error: ${response.status}`)
31
+ const data = await response.json()
32
+ this.accessToken = data.accessToken
33
+ return { success: true, expiresAt: data.expiresAt }
34
+ }
35
+ }
36
+
37
+ export const nextbankClient = new NextBankClient()
@@ -0,0 +1,53 @@
1
+ import snowflake from 'snowflake-sdk'
2
+ import type { Bind } from 'snowflake-sdk'
3
+ import { env } from '@/config/env'
4
+
5
+ const config = {
6
+ account: env.SNOWFLAKE_ACCOUNT,
7
+ username: env.SNOWFLAKE_USERNAME,
8
+ password: env.SNOWFLAKE_PASSWORD,
9
+ warehouse: env.SNOWFLAKE_WAREHOUSE,
10
+ database: env.SNOWFLAKE_DATABASE,
11
+ schema: env.SNOWFLAKE_SCHEMA,
12
+ role: env.SNOWFLAKE_ROLE,
13
+ }
14
+
15
+ export function createSnowflakeConnection() {
16
+ return snowflake.createConnection(config)
17
+ }
18
+
19
+ export async function connectSnowflake(): Promise<snowflake.Connection> {
20
+ return new Promise((resolve, reject) => {
21
+ const connection = createSnowflakeConnection()
22
+ connection.connect((err, conn) => {
23
+ if (err) reject(err)
24
+ else resolve(conn)
25
+ })
26
+ })
27
+ }
28
+
29
+ export async function executeQuery<T = unknown>(
30
+ connection: snowflake.Connection,
31
+ sqlText: string,
32
+ binds?: Bind[]
33
+ ): Promise<T[]> {
34
+ return new Promise((resolve, reject) => {
35
+ connection.execute({
36
+ sqlText,
37
+ binds: binds as any,
38
+ complete: (err, stmt, rows) => {
39
+ if (err) reject(err)
40
+ else resolve((rows || []) as T[])
41
+ },
42
+ })
43
+ })
44
+ }
45
+
46
+ export async function destroyConnection(connection: snowflake.Connection): Promise<void> {
47
+ return new Promise((resolve, reject) => {
48
+ connection.destroy((err) => {
49
+ if (err) reject(err)
50
+ else resolve()
51
+ })
52
+ })
53
+ }
@@ -0,0 +1,7 @@
1
+ import { createClient } from '@supabase/supabase-js'
2
+ import { env } from '@/config/env'
3
+
4
+ export const supabaseAdmin = createClient(
5
+ env.NEXT_PUBLIC_SUPABASE_URL,
6
+ env.SUPABASE_SERVICE_ROLE_KEY
7
+ )
@@ -0,0 +1,7 @@
1
+ import { createClient } from '@supabase/supabase-js'
2
+ import { env } from '@/config/env'
3
+
4
+ export const supabase = createClient(
5
+ env.NEXT_PUBLIC_SUPABASE_URL,
6
+ env.NEXT_PUBLIC_SUPABASE_ANON_KEY
7
+ )
@@ -0,0 +1,23 @@
1
+ import { createServerClient } from '@supabase/ssr'
2
+ import { cookies } from 'next/headers'
3
+ import { env } from '@/config/env'
4
+
5
+ export async function createClient() {
6
+ const cookieStore = await cookies()
7
+
8
+ return createServerClient(env.NEXT_PUBLIC_SUPABASE_URL, env.NEXT_PUBLIC_SUPABASE_ANON_KEY, {
9
+ cookies: {
10
+ getAll() {
11
+ return cookieStore.getAll()
12
+ },
13
+ setAll(cookiesToSet) {
14
+ try {
15
+ cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options))
16
+ } catch {
17
+ // The `setAll` method was called from a Server Component.
18
+ // This can be ignored if you have middleware refreshing user sessions.
19
+ }
20
+ },
21
+ },
22
+ })
23
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,9 @@
1
+ // Validators placeholder
2
+ import { z } from 'zod'
3
+
4
+ export const healthCheckSchema = z.object({
5
+ status: z.enum(['healthy', 'unhealthy', 'error', 'pending', 'unconfigured']),
6
+ responseTimeMs: z.number().optional(),
7
+ message: z.string().optional(),
8
+ error: z.string().optional(),
9
+ })
@@ -0,0 +1,22 @@
1
+ // Clerk middleware placeholder
2
+ import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
3
+
4
+ const isPublicRoute = createRouteMatcher([
5
+ '/sign-in(.*)',
6
+ '/sign-up(.*)',
7
+ '/api/webhooks(.*)',
8
+ '/api/ping',
9
+ ])
10
+
11
+ export default clerkMiddleware(async (auth, request) => {
12
+ if (!isPublicRoute(request)) {
13
+ await auth.protect()
14
+ }
15
+ })
16
+
17
+ export const config = {
18
+ matcher: [
19
+ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
20
+ '/(api|trpc)(.*)',
21
+ ],
22
+ }
@@ -0,0 +1,22 @@
1
+ import { Hono } from 'hono'
2
+ import { cors } from 'hono/cors'
3
+ import { logger } from 'hono/logger'
4
+ import { authMiddleware } from './middleware/auth'
5
+ import { healthRoutes } from './routes/health'
6
+ import { usersRoutes } from './routes/users'
7
+
8
+ const app = new Hono().basePath('/api')
9
+
10
+ app.use('*', logger())
11
+ app.use('*', cors())
12
+
13
+ // Public routes
14
+ app.get('/ping', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }))
15
+
16
+ // Protected routes - require authentication
17
+ app.use('*', authMiddleware)
18
+ app.route('/health', healthRoutes)
19
+ app.route('/users', usersRoutes)
20
+
21
+ export { app }
22
+ export type AppType = typeof app
@@ -0,0 +1,19 @@
1
+ // Clerk auth middleware for Hono placeholder
2
+ import { createMiddleware } from 'hono/factory'
3
+ import { getAuth } from '@clerk/nextjs/server'
4
+ import type { NextRequest } from 'next/server'
5
+
6
+ export const authMiddleware = createMiddleware(async (c, next) => {
7
+ const request = c.req.raw as NextRequest
8
+ const auth = getAuth(request)
9
+
10
+ if (!auth.userId) {
11
+ return c.json({ error: 'Unauthorized' }, 401)
12
+ }
13
+
14
+ c.set('auth', auth)
15
+ c.set('userId', auth.userId)
16
+ c.set('orgId', auth.orgId)
17
+
18
+ await next()
19
+ })
@@ -0,0 +1,4 @@
1
+ // Logger middleware placeholder
2
+ import { logger } from 'hono/logger'
3
+
4
+ export { logger }
@@ -0,0 +1,214 @@
1
+ import { Hono } from 'hono'
2
+ import { getAuth, clerkClient } from '@clerk/nextjs/server'
3
+ import type { NextRequest } from 'next/server'
4
+
5
+ export const clerkHealthRoutes = new Hono()
6
+
7
+ clerkHealthRoutes.get('/env', async (c) => {
8
+ const start = performance.now()
9
+ const secretKey = process.env.CLERK_SECRET_KEY
10
+ const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
11
+
12
+ if (secretKey && publishableKey) {
13
+ return c.json({
14
+ status: 'healthy',
15
+ responseTimeMs: Math.round(performance.now() - start),
16
+ message: 'Clerk environment variables are configured',
17
+ data: {
18
+ hasSecretKey: true,
19
+ hasPublishableKey: true,
20
+ },
21
+ })
22
+ }
23
+
24
+ return c.json(
25
+ {
26
+ status: 'error',
27
+ responseTimeMs: Math.round(performance.now() - start),
28
+ error: 'Missing Clerk environment variables',
29
+ data: {
30
+ hasSecretKey: !!secretKey,
31
+ hasPublishableKey: !!publishableKey,
32
+ },
33
+ },
34
+ 500
35
+ )
36
+ })
37
+
38
+ clerkHealthRoutes.get('/api-status', async (c) => {
39
+ const start = performance.now()
40
+ try {
41
+ const client = await clerkClient()
42
+ const count = await client.users.getCount()
43
+ return c.json({
44
+ status: 'healthy',
45
+ responseTimeMs: Math.round(performance.now() - start),
46
+ message: 'Successfully connected to Clerk Backend API',
47
+ data: { userCount: count },
48
+ })
49
+ } catch (error) {
50
+ return c.json(
51
+ {
52
+ status: 'error',
53
+ responseTimeMs: Math.round(performance.now() - start),
54
+ error: error instanceof Error ? error.message : 'Failed to connect to Clerk Backend API',
55
+ },
56
+ 500
57
+ )
58
+ }
59
+ })
60
+
61
+ clerkHealthRoutes.get('/user', async (c) => {
62
+ const start = performance.now()
63
+
64
+ try {
65
+ const auth = getAuth(c.req.raw as NextRequest)
66
+
67
+ if (!auth.userId) {
68
+ return c.json(
69
+ {
70
+ status: 'error',
71
+ responseTimeMs: Math.round(performance.now() - start),
72
+ error: 'No authenticated user found',
73
+ },
74
+ 401
75
+ )
76
+ }
77
+
78
+ const client = await clerkClient()
79
+ const user = await client.users.getUser(auth.userId)
80
+
81
+ return c.json({
82
+ status: 'healthy',
83
+ responseTimeMs: Math.round(performance.now() - start),
84
+ message: 'Successfully retrieved current user',
85
+ data: {
86
+ userId: auth.userId,
87
+ email: user.emailAddresses[0]?.emailAddress,
88
+ firstName: user.firstName,
89
+ lastName: user.lastName,
90
+ fullName: `${user.firstName} ${user.lastName}`,
91
+ hasOrg: !!auth.orgId,
92
+ },
93
+ })
94
+ } catch (error) {
95
+ return c.json(
96
+ {
97
+ status: 'error',
98
+ responseTimeMs: Math.round(performance.now() - start),
99
+ error: error instanceof Error ? error.message : 'Failed to get user',
100
+ },
101
+ 500
102
+ )
103
+ }
104
+ })
105
+
106
+ // Session route removed
107
+
108
+
109
+ clerkHealthRoutes.get('/org', async (c) => {
110
+ const start = performance.now()
111
+
112
+ try {
113
+ const auth = getAuth(c.req.raw as NextRequest)
114
+
115
+ if (!auth.userId) {
116
+ return c.json(
117
+ {
118
+ status: 'error',
119
+ responseTimeMs: Math.round(performance.now() - start),
120
+ error: 'No authenticated user found',
121
+ },
122
+ 401
123
+ )
124
+ }
125
+
126
+ if (!auth.orgId) {
127
+ return c.json({
128
+ status: 'healthy',
129
+ responseTimeMs: Math.round(performance.now() - start),
130
+ message: 'User is not a member of any organization',
131
+ data: { orgId: null },
132
+ })
133
+ }
134
+
135
+ return c.json({
136
+ status: 'healthy',
137
+ responseTimeMs: Math.round(performance.now() - start),
138
+ message: 'Successfully retrieved organization membership',
139
+ data: {
140
+ orgId: auth.orgId,
141
+ orgRole: auth.orgRole,
142
+ },
143
+ })
144
+ } catch (error) {
145
+ return c.json(
146
+ {
147
+ status: 'error',
148
+ responseTimeMs: Math.round(performance.now() - start),
149
+ error: error instanceof Error ? error.message : 'Failed to get organization',
150
+ },
151
+ 500
152
+ )
153
+ }
154
+ })
155
+
156
+ clerkHealthRoutes.get('/members', async (c) => {
157
+ const start = performance.now()
158
+
159
+ try {
160
+ const auth = getAuth(c.req.raw as NextRequest)
161
+
162
+ if (!auth.userId) {
163
+ return c.json(
164
+ {
165
+ status: 'error',
166
+ responseTimeMs: Math.round(performance.now() - start),
167
+ error: 'No authenticated user found',
168
+ },
169
+ 401
170
+ )
171
+ }
172
+
173
+ if (!auth.orgId) {
174
+ return c.json({
175
+ status: 'healthy',
176
+ responseTimeMs: Math.round(performance.now() - start),
177
+ message: 'User is not a member of any organization',
178
+ data: { members: [] },
179
+ })
180
+ }
181
+
182
+ // Note: In a real implementation, you would use Clerk's Backend API to fetch org members
183
+ // This is a simplified check that verifies org membership exists
184
+ const client = await clerkClient()
185
+ const members = await client.organizations.getOrganizationMembershipList({ organizationId: auth.orgId })
186
+
187
+ return c.json({
188
+ status: 'healthy',
189
+ responseTimeMs: Math.round(performance.now() - start),
190
+ message: 'Successfully verified organization membership',
191
+ data: {
192
+ orgId: auth.orgId,
193
+ orgRole: auth.orgRole,
194
+ count: members.totalCount,
195
+ members: members.data.map((m) => ({
196
+ id: m.id,
197
+ userId: m.publicUserData?.userId,
198
+ role: m.role,
199
+ name: `${m.publicUserData?.firstName} ${m.publicUserData?.lastName}`,
200
+ email: m.publicUserData?.identifier,
201
+ })),
202
+ },
203
+ })
204
+ } catch (error) {
205
+ return c.json(
206
+ {
207
+ status: 'error',
208
+ responseTimeMs: Math.round(performance.now() - start),
209
+ error: error instanceof Error ? error.message : 'Failed to get organization members',
210
+ },
211
+ 500
212
+ )
213
+ }
214
+ })
@@ -0,0 +1,117 @@
1
+ import { Hono } from 'hono'
2
+ import { db } from '@/server/db'
3
+ import { healthCheckTests } from '@/server/db/schema/health-checks'
4
+ import { eq } from 'drizzle-orm'
5
+
6
+ export const databaseHealthRoutes = new Hono()
7
+
8
+ const TEST_KEY = 'health-check-test'
9
+
10
+ databaseHealthRoutes.post('/write', async (c) => {
11
+ const start = performance.now()
12
+
13
+ try {
14
+ // Delete any existing test row first
15
+ await db.delete(healthCheckTests).where(eq(healthCheckTests.testKey, TEST_KEY))
16
+
17
+ // Insert a new test row
18
+ const result = await db
19
+ .insert(healthCheckTests)
20
+ .values({
21
+ testKey: TEST_KEY,
22
+ testValue: `test-${Date.now()}`,
23
+ })
24
+ .returning()
25
+
26
+ return c.json({
27
+ status: 'healthy',
28
+ responseTimeMs: Math.round(performance.now() - start),
29
+ message: 'Successfully wrote test row to database',
30
+ data: {
31
+ id: result[0]?.id,
32
+ testKey: result[0]?.testKey,
33
+ },
34
+ })
35
+ } catch (error) {
36
+ return c.json(
37
+ {
38
+ status: 'error',
39
+ responseTimeMs: Math.round(performance.now() - start),
40
+ error: error instanceof Error ? error.message : 'Failed to write to database',
41
+ },
42
+ 500
43
+ )
44
+ }
45
+ })
46
+
47
+ databaseHealthRoutes.get('/read', async (c) => {
48
+ const start = performance.now()
49
+
50
+ try {
51
+ const result = await db
52
+ .select()
53
+ .from(healthCheckTests)
54
+ .where(eq(healthCheckTests.testKey, TEST_KEY))
55
+ .limit(1)
56
+
57
+ if (result.length === 0) {
58
+ return c.json({
59
+ status: 'healthy',
60
+ responseTimeMs: Math.round(performance.now() - start),
61
+ message: 'Database connection successful, but no test row found',
62
+ data: { found: false },
63
+ })
64
+ }
65
+
66
+ return c.json({
67
+ status: 'healthy',
68
+ responseTimeMs: Math.round(performance.now() - start),
69
+ message: 'Successfully read test row from database',
70
+ data: {
71
+ found: true,
72
+ id: result[0]?.id,
73
+ testKey: result[0]?.testKey,
74
+ createdAt: result[0]?.createdAt,
75
+ },
76
+ })
77
+ } catch (error) {
78
+ return c.json(
79
+ {
80
+ status: 'error',
81
+ responseTimeMs: Math.round(performance.now() - start),
82
+ error: error instanceof Error ? error.message : 'Failed to read from database',
83
+ },
84
+ 500
85
+ )
86
+ }
87
+ })
88
+
89
+ databaseHealthRoutes.delete('/delete', async (c) => {
90
+ const start = performance.now()
91
+
92
+ try {
93
+ const result = await db
94
+ .delete(healthCheckTests)
95
+ .where(eq(healthCheckTests.testKey, TEST_KEY))
96
+ .returning()
97
+
98
+ return c.json({
99
+ status: 'healthy',
100
+ responseTimeMs: Math.round(performance.now() - start),
101
+ message: 'Successfully deleted test row from database',
102
+ data: {
103
+ deleted: result.length > 0,
104
+ count: result.length,
105
+ },
106
+ })
107
+ } catch (error) {
108
+ return c.json(
109
+ {
110
+ status: 'error',
111
+ responseTimeMs: Math.round(performance.now() - start),
112
+ error: error instanceof Error ? error.message : 'Failed to delete from database',
113
+ },
114
+ 500
115
+ )
116
+ }
117
+ })