@odvi/create-dtt-framework 0.1.3 → 0.1.6
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/package.json +3 -2
- package/template/.env.example +106 -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 +862 -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 +646 -0
- package/template/docs/framework/health-check-system.md +583 -0
- package/template/docs/framework/implementation.md +559 -0
- package/template/docs/framework/snowflake-integration.md +594 -0
- package/template/docs/framework/state-management.md +615 -0
- package/template/docs/framework/supabase-integration.md +582 -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 +11 -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 +164 -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 +374 -0
- package/template/src/features/health-check/config.ts +71 -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 +67 -0
- package/template/src/lib/snowflake/client.ts +102 -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 +141 -0
- package/template/src/server/api/routes/health/edge-functions.ts +107 -0
- package/template/src/server/api/routes/health/framework.ts +48 -0
- package/template/src/server/api/routes/health/index.ts +102 -0
- package/template/src/server/api/routes/health/nextbank.ts +46 -0
- package/template/src/server/api/routes/health/snowflake.ts +83 -0
- package/template/src/server/api/routes/health/storage.ts +177 -0
- package/template/src/server/api/routes/users.ts +79 -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 +1 -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,141 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { supabaseAdmin } from '@/lib/supabase/admin'
|
|
3
|
+
|
|
4
|
+
export const databaseHealthRoutes = new Hono()
|
|
5
|
+
|
|
6
|
+
const TEST_KEY = 'health-check-test'
|
|
7
|
+
|
|
8
|
+
databaseHealthRoutes.post('/write', async (c) => {
|
|
9
|
+
const start = performance.now()
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// Delete any existing test row first
|
|
13
|
+
await supabaseAdmin.from('health_check_tests').delete().eq('test_key', TEST_KEY)
|
|
14
|
+
|
|
15
|
+
// Insert a new test row
|
|
16
|
+
const { data: result, error } = await supabaseAdmin
|
|
17
|
+
.from('health_check_tests')
|
|
18
|
+
.insert({
|
|
19
|
+
test_key: TEST_KEY,
|
|
20
|
+
test_value: `test-${Date.now()}`,
|
|
21
|
+
})
|
|
22
|
+
.select()
|
|
23
|
+
|
|
24
|
+
if (error) throw error
|
|
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]?.test_key,
|
|
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 { data: result, error } = await supabaseAdmin
|
|
52
|
+
.from('health_check_tests')
|
|
53
|
+
.select('*')
|
|
54
|
+
.eq('test_key', TEST_KEY)
|
|
55
|
+
.limit(1)
|
|
56
|
+
|
|
57
|
+
if (error) throw error
|
|
58
|
+
|
|
59
|
+
if (!result || result.length === 0) {
|
|
60
|
+
return c.json({
|
|
61
|
+
status: 'healthy',
|
|
62
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
63
|
+
message: 'Database connection successful, but no test row found',
|
|
64
|
+
data: { found: false },
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return c.json({
|
|
69
|
+
status: 'healthy',
|
|
70
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
71
|
+
message: 'Successfully read test row from database',
|
|
72
|
+
data: {
|
|
73
|
+
found: true,
|
|
74
|
+
id: result[0]?.id,
|
|
75
|
+
testKey: result[0]?.test_key,
|
|
76
|
+
createdAt: result[0]?.created_at,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return c.json(
|
|
81
|
+
{
|
|
82
|
+
status: 'error',
|
|
83
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
84
|
+
error: error instanceof Error ? error.message : 'Failed to read from database',
|
|
85
|
+
},
|
|
86
|
+
500
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
databaseHealthRoutes.delete('/delete', async (c) => {
|
|
92
|
+
const start = performance.now()
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Check if row exists first
|
|
96
|
+
const { data: result, error: readError } = await supabaseAdmin
|
|
97
|
+
.from('health_check_tests')
|
|
98
|
+
.select('*')
|
|
99
|
+
.eq('test_key', TEST_KEY)
|
|
100
|
+
.limit(1)
|
|
101
|
+
|
|
102
|
+
if (readError) throw readError
|
|
103
|
+
|
|
104
|
+
if (!result || result.length === 0) {
|
|
105
|
+
return c.json({
|
|
106
|
+
status: 'healthy',
|
|
107
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
108
|
+
message: 'Database connection successful, but no test row to delete',
|
|
109
|
+
data: { deleted: false },
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Delete the row
|
|
114
|
+
const { error: deleteError } = await supabaseAdmin
|
|
115
|
+
.from('health_check_tests')
|
|
116
|
+
.delete()
|
|
117
|
+
.eq('test_key', TEST_KEY)
|
|
118
|
+
|
|
119
|
+
if (deleteError) throw deleteError
|
|
120
|
+
|
|
121
|
+
return c.json({
|
|
122
|
+
status: 'healthy',
|
|
123
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
124
|
+
message: 'Successfully deleted test row from database',
|
|
125
|
+
data: {
|
|
126
|
+
deleted: true,
|
|
127
|
+
id: result[0]?.id,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return c.json(
|
|
132
|
+
{
|
|
133
|
+
status: 'error',
|
|
134
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
135
|
+
error: error instanceof Error ? error.message : 'Failed to delete from database',
|
|
136
|
+
},
|
|
137
|
+
500
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
// Invoke the 'health-check' edge function
|
|
12
|
+
const { data, error } = await supabaseAdmin.functions.invoke('health-check', {
|
|
13
|
+
body: { message: 'ping' },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
if (error) {
|
|
17
|
+
// Return detailed error information
|
|
18
|
+
const errorDetails = error as any
|
|
19
|
+
return c.json({
|
|
20
|
+
status: 'error',
|
|
21
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
22
|
+
message: 'Edge Function invocation failed',
|
|
23
|
+
data: {
|
|
24
|
+
error: error.message,
|
|
25
|
+
context: errorDetails.context || 'No context available',
|
|
26
|
+
hint: 'Ensure the function is deployed: "supabase functions deploy health-check"'
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return c.json({
|
|
32
|
+
status: 'healthy',
|
|
33
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
34
|
+
message: 'Successfully invoked edge function',
|
|
35
|
+
data,
|
|
36
|
+
})
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return c.json(
|
|
39
|
+
{
|
|
40
|
+
status: 'error',
|
|
41
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
42
|
+
error: error instanceof Error ? error.message : 'Failed to ping edge function',
|
|
43
|
+
},
|
|
44
|
+
500
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
edgeFunctionsHealthRoutes.get('/auth', async (c) => {
|
|
50
|
+
const start = performance.now()
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Verify that we can create a valid auth header and invoke the function
|
|
54
|
+
const authHeader = c.req.header('authorization')
|
|
55
|
+
|
|
56
|
+
// If no auth header is provided to the health check endpoint, we can't test the downstream auth
|
|
57
|
+
if (!authHeader) {
|
|
58
|
+
return c.json({
|
|
59
|
+
status: 'warning', // Changed from error to warning/skipped if user isn't logged in
|
|
60
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
61
|
+
message: 'No auth header provided - skipping auth propagation check',
|
|
62
|
+
data: {
|
|
63
|
+
skipped: true,
|
|
64
|
+
reason: 'Client request did not include Authorization header',
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { data, error } = await supabaseAdmin.functions.invoke('health-check', {
|
|
70
|
+
body: { message: 'auth-check' },
|
|
71
|
+
headers: {
|
|
72
|
+
// We send the auth header in a custom header to verify it can be passed through
|
|
73
|
+
// We avoid overriding 'Authorization' to prevent Supabase Gateway from rejecting
|
|
74
|
+
// the request if the Clerk token hasn't been configured in Supabase yet.
|
|
75
|
+
'x-test-auth': authHeader,
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (error) {
|
|
80
|
+
// If we get an error from the function, it might be due to auth validation failure
|
|
81
|
+
// or the function crashing. We want to expose the error message.
|
|
82
|
+
const errorDetails = error as any
|
|
83
|
+
throw new Error(`Edge function invocation failed: ${error.message || 'Unknown error'}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return c.json({
|
|
87
|
+
status: 'healthy',
|
|
88
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
89
|
+
message: 'Successfully verified auth header with edge function',
|
|
90
|
+
data,
|
|
91
|
+
})
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Check if the error is due to a 401/403 from the function itself (which would come as an error response data if handled,
|
|
94
|
+
// or thrown if using invoke with certain options, but here likely a re-throw)
|
|
95
|
+
return c.json(
|
|
96
|
+
{
|
|
97
|
+
status: 'error',
|
|
98
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
99
|
+
error: error instanceof Error ? error.message : 'Failed to test auth header',
|
|
100
|
+
data: {
|
|
101
|
+
details: error instanceof Error ? error.stack : undefined
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
500
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { supabaseAdmin } from '@/lib/supabase/admin'
|
|
3
|
+
|
|
4
|
+
export const frameworkHealthRoutes = new Hono()
|
|
5
|
+
|
|
6
|
+
frameworkHealthRoutes.get('/hono', (c) => {
|
|
7
|
+
const start = performance.now()
|
|
8
|
+
return c.json({
|
|
9
|
+
status: 'healthy',
|
|
10
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
11
|
+
message: 'Hono framework is running correctly',
|
|
12
|
+
data: {
|
|
13
|
+
version: '4.11.3', // matching package.json
|
|
14
|
+
environment: process.env.NODE_ENV,
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
frameworkHealthRoutes.get('/drizzle', async (c) => {
|
|
20
|
+
const start = performance.now()
|
|
21
|
+
try {
|
|
22
|
+
// Note: Edge Runtime doesn't support direct Drizzle + postgres.js connection
|
|
23
|
+
// We check Supabase connection instead as a proxy for DB health
|
|
24
|
+
const { error } = await supabaseAdmin.from('health_check_tests').select('count').limit(1)
|
|
25
|
+
|
|
26
|
+
if (error) throw error
|
|
27
|
+
|
|
28
|
+
return c.json({
|
|
29
|
+
status: 'healthy',
|
|
30
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
31
|
+
message: 'Database access via Supabase Client is working (Edge compatible)',
|
|
32
|
+
data: {
|
|
33
|
+
mode: 'http-client',
|
|
34
|
+
platform: 'supabase',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return c.json(
|
|
39
|
+
{
|
|
40
|
+
status: 'error',
|
|
41
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
42
|
+
error: error instanceof Error ? error.message : 'Database check failed',
|
|
43
|
+
},
|
|
44
|
+
500
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
@@ -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,46 @@
|
|
|
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 || env.NEXTBANK_API === '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
|
+
// Generate fingerprint based on request headers
|
|
19
|
+
const userAgent = c.req.header('user-agent') || 'unknown'
|
|
20
|
+
const ipAddress = c.req.header('x-forwarded-for')?.split(',')[0] ||
|
|
21
|
+
c.req.header('x-real-ip') ||
|
|
22
|
+
'127.0.0.1'
|
|
23
|
+
const acceptLanguage = c.req.header('accept-language') || ''
|
|
24
|
+
|
|
25
|
+
const fingerprintString = `${userAgent}-${ipAddress}-${acceptLanguage}`
|
|
26
|
+
const fingerprint = Buffer.from(fingerprintString).toString('base64')
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const result = await nextbankClient.ping(fingerprint)
|
|
30
|
+
return c.json({
|
|
31
|
+
status: 'healthy',
|
|
32
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
33
|
+
message: 'Successfully pinged NextBank API',
|
|
34
|
+
data: result,
|
|
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 : 'Ping failed',
|
|
42
|
+
},
|
|
43
|
+
500
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
@@ -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
|
+
authenticator: env.SNOWFLAKE_AUTHENTICATOR,
|
|
30
|
+
role: env.SNOWFLAKE_ROLE,
|
|
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
|
+
})
|