@odvi/create-dtt-framework 0.1.5 → 0.1.7
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/index.js +0 -0
- package/dist/utils/template.js +1 -0
- package/dist/utils/template.js.map +1 -1
- package/package.json +50 -51
- package/template/.env.example +106 -103
- package/template/README.md +157 -7
- package/template/docs/framework/deployment/environment-variables.md +3 -4
- package/template/docs/framework/environment-variables.md +13 -25
- package/template/docs/framework/health-check-system.md +1 -0
- package/template/docs/framework/snowflake-integration.md +19 -16
- package/template/docs/framework/supabase-integration.md +1 -0
- package/template/drizzle.config.ts +0 -1
- package/template/src/config/__tests__/env.test.ts +6 -8
- package/template/src/config/env.ts +8 -8
- package/template/src/features/health-check/components/health-dashboard.tsx +12 -1
- package/template/src/features/health-check/config.ts +0 -1
- package/template/src/lib/nextbank/client.ts +53 -23
- package/template/src/lib/snowflake/client.ts +64 -15
- package/template/src/server/api/routes/health/database.ts +49 -25
- package/template/src/server/api/routes/health/edge-functions.ts +58 -26
- package/template/src/server/api/routes/health/framework.ts +10 -7
- package/template/src/server/api/routes/health/nextbank.ts +12 -33
- package/template/src/server/api/routes/health/snowflake.ts +2 -2
- package/template/src/server/api/routes/health/storage.ts +14 -0
- package/template/src/server/api/routes/users.ts +14 -30
- package/template/src/server/db/schema.ts +1 -26
- package/template/supabase/.temp/cli-latest +1 -0
- package/template/supabase/functions/health-check/index.ts +84 -0
- package/template/tsconfig.json +1 -1
|
@@ -7,7 +7,7 @@ export const nextbankHealthRoutes = new Hono()
|
|
|
7
7
|
nextbankHealthRoutes.get('/ping', async (c) => {
|
|
8
8
|
const start = performance.now()
|
|
9
9
|
|
|
10
|
-
if (!env.
|
|
10
|
+
if (!env.NEXTBANK_API || env.NEXTBANK_API === 'placeholder') {
|
|
11
11
|
return c.json({
|
|
12
12
|
status: 'unconfigured',
|
|
13
13
|
responseTimeMs: Math.round(performance.now() - start),
|
|
@@ -15,43 +15,22 @@ nextbankHealthRoutes.get('/ping', async (c) => {
|
|
|
15
15
|
})
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
})
|
|
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') || ''
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
const
|
|
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
|
-
}
|
|
25
|
+
const fingerprintString = `${userAgent}-${ipAddress}-${acceptLanguage}`
|
|
26
|
+
const fingerprint = Buffer.from(fingerprintString).toString('base64')
|
|
48
27
|
|
|
49
28
|
try {
|
|
50
|
-
const result = await nextbankClient.
|
|
29
|
+
const result = await nextbankClient.ping(fingerprint)
|
|
51
30
|
return c.json({
|
|
52
31
|
status: 'healthy',
|
|
53
32
|
responseTimeMs: Math.round(performance.now() - start),
|
|
54
|
-
message: 'Successfully
|
|
33
|
+
message: 'Successfully pinged NextBank API',
|
|
55
34
|
data: result,
|
|
56
35
|
})
|
|
57
36
|
} catch (error) {
|
|
@@ -59,7 +38,7 @@ nextbankHealthRoutes.get('/auth', async (c) => {
|
|
|
59
38
|
{
|
|
60
39
|
status: 'error',
|
|
61
40
|
responseTimeMs: Math.round(performance.now() - start),
|
|
62
|
-
error: error instanceof Error ? error.message : '
|
|
41
|
+
error: error instanceof Error ? error.message : 'Ping failed',
|
|
63
42
|
},
|
|
64
43
|
500
|
|
65
44
|
)
|
|
@@ -26,8 +26,8 @@ snowflakeHealthRoutes.get('/connect', async (c) => {
|
|
|
26
26
|
data: {
|
|
27
27
|
account: env.SNOWFLAKE_ACCOUNT,
|
|
28
28
|
warehouse: env.SNOWFLAKE_WAREHOUSE,
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
authenticator: env.SNOWFLAKE_AUTHENTICATOR,
|
|
30
|
+
role: env.SNOWFLAKE_ROLE,
|
|
31
31
|
},
|
|
32
32
|
})
|
|
33
33
|
} catch (error) {
|
|
@@ -86,6 +86,20 @@ storageHealthRoutes.get('/download', async (c) => {
|
|
|
86
86
|
.from(BUCKET_NAME)
|
|
87
87
|
.createSignedUrl(TEST_FILE_PATH, 60)
|
|
88
88
|
|
|
89
|
+
// Handle "Object not found" as a success state for health check
|
|
90
|
+
// since connectivity is working, but the file just isn't there
|
|
91
|
+
if (error && error.message.includes('Object not found')) {
|
|
92
|
+
return c.json({
|
|
93
|
+
status: 'healthy',
|
|
94
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
95
|
+
message: 'Storage connection successful, but no test file found to download',
|
|
96
|
+
data: {
|
|
97
|
+
found: false,
|
|
98
|
+
path: TEST_FILE_PATH,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
if (error) throw error
|
|
90
104
|
|
|
91
105
|
return c.json({
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
-
import {
|
|
3
|
-
import { users } from '@/server/db/schema/users'
|
|
4
|
-
import { eq, desc } from 'drizzle-orm'
|
|
2
|
+
import { supabaseAdmin } from '@/lib/supabase/admin'
|
|
5
3
|
|
|
6
4
|
export const usersRoutes = new Hono()
|
|
7
5
|
|
|
@@ -9,21 +7,14 @@ usersRoutes.get('/', async (c) => {
|
|
|
9
7
|
const start = performance.now()
|
|
10
8
|
|
|
11
9
|
try {
|
|
12
|
-
const allUsers = await
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
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))
|
|
10
|
+
const { data: allUsers, error } = await supabaseAdmin
|
|
11
|
+
.from('users')
|
|
12
|
+
.select('id, email, first_name, last_name, image_url, clerk_org_id, created_at, updated_at')
|
|
13
|
+
.order('created_at', { ascending: false })
|
|
25
14
|
.limit(100)
|
|
26
15
|
|
|
16
|
+
if (error) throw error
|
|
17
|
+
|
|
27
18
|
return c.json({
|
|
28
19
|
status: 'healthy',
|
|
29
20
|
responseTimeMs: Math.round(performance.now() - start),
|
|
@@ -50,22 +41,15 @@ usersRoutes.get('/:id', async (c) => {
|
|
|
50
41
|
const id = c.req.param('id')
|
|
51
42
|
|
|
52
43
|
try {
|
|
53
|
-
const user = await
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
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))
|
|
44
|
+
const { data: user, error } = await supabaseAdmin
|
|
45
|
+
.from('users')
|
|
46
|
+
.select('id, email, first_name, last_name, image_url, clerk_org_id, created_at, updated_at')
|
|
47
|
+
.eq('id', id)
|
|
66
48
|
.limit(1)
|
|
67
49
|
|
|
68
|
-
if (
|
|
50
|
+
if (error) throw error
|
|
51
|
+
|
|
52
|
+
if (!user || user.length === 0) {
|
|
69
53
|
return c.json(
|
|
70
54
|
{
|
|
71
55
|
status: 'error',
|
|
@@ -1,26 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// https://orm.drizzle.team/docs/sql-schema-declaration
|
|
3
|
-
|
|
4
|
-
import { index, pgTableCreator } from "drizzle-orm/pg-core";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
|
8
|
-
* database instance for multiple projects.
|
|
9
|
-
*
|
|
10
|
-
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
|
11
|
-
*/
|
|
12
|
-
export const createTable = pgTableCreator((name) => `dtt-framework_${name}`);
|
|
13
|
-
|
|
14
|
-
export const posts = createTable(
|
|
15
|
-
"post",
|
|
16
|
-
(d) => ({
|
|
17
|
-
id: d.integer().primaryKey().generatedByDefaultAsIdentity(),
|
|
18
|
-
name: d.varchar({ length: 256 }),
|
|
19
|
-
createdAt: d
|
|
20
|
-
.timestamp({ withTimezone: true })
|
|
21
|
-
.$defaultFn(() => /* @__PURE__ */ new Date())
|
|
22
|
-
.notNull(),
|
|
23
|
-
updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()),
|
|
24
|
-
}),
|
|
25
|
-
(t) => [index("name_idx").on(t.name)],
|
|
26
|
-
);
|
|
1
|
+
export * from "./schema/index";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v2.67.1
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Follow this setup guide to deploy the function:
|
|
2
|
+
// https://supabase.com/docs/guides/functions/deploy
|
|
3
|
+
|
|
4
|
+
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
|
|
5
|
+
|
|
6
|
+
const corsHeaders = {
|
|
7
|
+
'Access-Control-Allow-Origin': '*',
|
|
8
|
+
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
serve(async (req) => {
|
|
12
|
+
if (req.method === 'OPTIONS') {
|
|
13
|
+
return new Response('ok', { headers: corsHeaders })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const { message } = await req.json()
|
|
18
|
+
let authHeader = req.headers.get('Authorization')
|
|
19
|
+
const testAuthHeader = req.headers.get('x-test-auth')
|
|
20
|
+
|
|
21
|
+
// Handle ping check
|
|
22
|
+
if (message === 'ping') {
|
|
23
|
+
return new Response(
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
message: 'pong',
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
}),
|
|
28
|
+
{
|
|
29
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
30
|
+
status: 200,
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Handle auth check
|
|
36
|
+
if (message === 'auth-check') {
|
|
37
|
+
// Prioritize test header if present (allows testing without bypassing Supabase Gateway auth)
|
|
38
|
+
if (testAuthHeader) {
|
|
39
|
+
authHeader = testAuthHeader
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!authHeader) {
|
|
43
|
+
return new Response(
|
|
44
|
+
JSON.stringify({ error: 'No authorization header provided' }),
|
|
45
|
+
{
|
|
46
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
47
|
+
status: 401,
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Response(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
message: 'Auth header received',
|
|
55
|
+
hasAuth: true,
|
|
56
|
+
// Don't log/return the full token in production for security
|
|
57
|
+
tokenPrefix: authHeader.substring(0, 10) + '...',
|
|
58
|
+
source: testAuthHeader ? 'x-test-auth' : 'Authorization'
|
|
59
|
+
}),
|
|
60
|
+
{
|
|
61
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
62
|
+
status: 200,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return new Response(
|
|
68
|
+
JSON.stringify({ error: 'Unknown message type' }),
|
|
69
|
+
{
|
|
70
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
71
|
+
status: 400,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return new Response(
|
|
76
|
+
JSON.stringify({ error: error.message }),
|
|
77
|
+
{
|
|
78
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
79
|
+
status: 400,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
package/template/tsconfig.json
CHANGED
|
@@ -39,5 +39,5 @@
|
|
|
39
39
|
"**/*.js",
|
|
40
40
|
".next/types/**/*.ts"
|
|
41
41
|
],
|
|
42
|
-
"exclude": ["node_modules", "generated", "cli", "src/test", "**/*.test.ts", "**/*.test.tsx", "vitest.config.ts", "drizzle.config.ts"]
|
|
42
|
+
"exclude": ["node_modules", "generated", "cli", "src/test", "**/*.test.ts", "**/*.test.tsx", "vitest.config.ts", "drizzle.config.ts", "supabase"]
|
|
43
43
|
}
|