@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,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,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
|
+
});
|