@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.
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +16 -13
- package/dist/commands/create.js.map +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +5 -0
- package/dist/utils/template.js.map +1 -1
- package/package.json +3 -2
- package/template/.env.example +103 -0
- package/template/components.json +22 -0
- package/template/docs/framework/01-overview.md +289 -0
- package/template/docs/framework/02-techstack.md +503 -0
- package/template/docs/framework/api-layer.md +681 -0
- package/template/docs/framework/clerk-authentication.md +649 -0
- package/template/docs/framework/cli-installation.md +564 -0
- package/template/docs/framework/deployment/ci-cd.md +907 -0
- package/template/docs/framework/deployment/digitalocean.md +991 -0
- package/template/docs/framework/deployment/domain-setup.md +972 -0
- package/template/docs/framework/deployment/environment-variables.md +863 -0
- package/template/docs/framework/deployment/monitoring.md +927 -0
- package/template/docs/framework/deployment/production-checklist.md +649 -0
- package/template/docs/framework/deployment/vercel.md +791 -0
- package/template/docs/framework/environment-variables.md +658 -0
- package/template/docs/framework/health-check-system.md +582 -0
- package/template/docs/framework/implementation.md +559 -0
- package/template/docs/framework/snowflake-integration.md +591 -0
- package/template/docs/framework/state-management.md +615 -0
- package/template/docs/framework/supabase-integration.md +581 -0
- package/template/docs/framework/testing-guide.md +544 -0
- package/template/docs/framework/what-did-i-miss.md +526 -0
- package/template/drizzle.config.ts +12 -0
- package/template/next.config.js +21 -0
- package/template/postcss.config.js +5 -0
- package/template/prettier.config.js +4 -0
- package/template/public/favicon.ico +0 -0
- package/template/src/app/(auth)/layout.tsx +4 -0
- package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
- package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
- package/template/src/app/(dashboard)/health/page.tsx +16 -0
- package/template/src/app/(dashboard)/layout.tsx +17 -0
- package/template/src/app/api/[[...route]]/route.ts +11 -0
- package/template/src/app/api/debug-files/route.ts +33 -0
- package/template/src/app/api/webhooks/clerk/route.ts +112 -0
- package/template/src/app/layout.tsx +28 -0
- package/template/src/app/page.tsx +12 -0
- package/template/src/app/providers.tsx +20 -0
- package/template/src/components/layouts/navbar.tsx +14 -0
- package/template/src/components/shared/loading-spinner.tsx +6 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +62 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/collapsible.tsx +33 -0
- package/template/src/components/ui/scroll-area.tsx +58 -0
- package/template/src/components/ui/sheet.tsx +139 -0
- package/template/src/config/__tests__/env.test.ts +166 -0
- package/template/src/config/__tests__/site.test.ts +46 -0
- package/template/src/config/env.ts +36 -0
- package/template/src/config/site.ts +10 -0
- package/template/src/env.js +44 -0
- package/template/src/features/__tests__/health-check-config.test.ts +142 -0
- package/template/src/features/__tests__/health-check-types.test.ts +201 -0
- package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
- package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
- package/template/src/features/documentation/index.tsx +92 -0
- package/template/src/features/documentation/utils/doc-loader.ts +177 -0
- package/template/src/features/health-check/components/health-dashboard.tsx +363 -0
- package/template/src/features/health-check/config.ts +72 -0
- package/template/src/features/health-check/index.ts +4 -0
- package/template/src/features/health-check/stores/health-store.ts +14 -0
- package/template/src/features/health-check/types.ts +18 -0
- package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
- package/template/src/hooks/queries/use-health-checks.ts +16 -0
- package/template/src/hooks/utils/use-debounce.ts +20 -0
- package/template/src/lib/__tests__/utils.test.ts +52 -0
- package/template/src/lib/__tests__/validators.test.ts +114 -0
- package/template/src/lib/nextbank/client.ts +37 -0
- package/template/src/lib/snowflake/client.ts +53 -0
- package/template/src/lib/supabase/admin.ts +7 -0
- package/template/src/lib/supabase/client.ts +7 -0
- package/template/src/lib/supabase/server.ts +23 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/lib/validators.ts +9 -0
- package/template/src/middleware.ts +22 -0
- package/template/src/server/api/index.ts +22 -0
- package/template/src/server/api/middleware/auth.ts +19 -0
- package/template/src/server/api/middleware/logger.ts +4 -0
- package/template/src/server/api/routes/health/clerk.ts +214 -0
- package/template/src/server/api/routes/health/database.ts +117 -0
- package/template/src/server/api/routes/health/edge-functions.ts +75 -0
- package/template/src/server/api/routes/health/framework.ts +45 -0
- package/template/src/server/api/routes/health/index.ts +102 -0
- package/template/src/server/api/routes/health/nextbank.ts +67 -0
- package/template/src/server/api/routes/health/snowflake.ts +83 -0
- package/template/src/server/api/routes/health/storage.ts +163 -0
- package/template/src/server/api/routes/users.ts +95 -0
- package/template/src/server/db/index.ts +17 -0
- package/template/src/server/db/queries/users.ts +8 -0
- package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
- package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
- package/template/src/server/db/schema/health-checks.ts +11 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/users.ts +16 -0
- package/template/src/server/db/schema.ts +26 -0
- package/template/src/stores/__tests__/ui-store.test.ts +87 -0
- package/template/src/stores/ui-store.ts +14 -0
- package/template/src/styles/globals.css +129 -0
- package/template/src/test/mocks/clerk.ts +35 -0
- package/template/src/test/mocks/snowflake.ts +28 -0
- package/template/src/test/mocks/supabase.ts +37 -0
- package/template/src/test/setup.ts +69 -0
- package/template/src/test/utils/test-helpers.ts +158 -0
- package/template/src/types/index.ts +14 -0
- package/template/tsconfig.json +43 -0
- 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,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,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,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
|
+
})
|