@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,177 @@
|
|
|
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
|
+
// 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
|
+
|
|
103
|
+
if (error) throw error
|
|
104
|
+
|
|
105
|
+
return c.json({
|
|
106
|
+
status: 'healthy',
|
|
107
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
108
|
+
message: 'Successfully generated download URL for test file',
|
|
109
|
+
data: {
|
|
110
|
+
found: true,
|
|
111
|
+
signedUrl: data?.signedUrl,
|
|
112
|
+
path: TEST_FILE_PATH,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return c.json(
|
|
117
|
+
{
|
|
118
|
+
status: 'error',
|
|
119
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
120
|
+
error: error instanceof Error ? error.message : 'Failed to download from storage',
|
|
121
|
+
},
|
|
122
|
+
500
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
storageHealthRoutes.delete('/delete', async (c) => {
|
|
128
|
+
const start = performance.now()
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Check if file exists first
|
|
132
|
+
const { data: fileData, error: checkError } = await supabaseAdmin.storage
|
|
133
|
+
.from(BUCKET_NAME)
|
|
134
|
+
.list('health-check', {
|
|
135
|
+
limit: 10,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (checkError) throw checkError
|
|
139
|
+
|
|
140
|
+
const testFile = fileData?.find((f) => f.name === 'test-file.txt')
|
|
141
|
+
|
|
142
|
+
if (!testFile) {
|
|
143
|
+
return c.json({
|
|
144
|
+
status: 'healthy',
|
|
145
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
146
|
+
message: 'Storage connection successful, but no test file to delete',
|
|
147
|
+
data: { deleted: false },
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Delete test file
|
|
152
|
+
const { error } = await supabaseAdmin.storage
|
|
153
|
+
.from(BUCKET_NAME)
|
|
154
|
+
.remove([TEST_FILE_PATH])
|
|
155
|
+
|
|
156
|
+
if (error) throw error
|
|
157
|
+
|
|
158
|
+
return c.json({
|
|
159
|
+
status: 'healthy',
|
|
160
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
161
|
+
message: 'Successfully deleted test file from storage',
|
|
162
|
+
data: {
|
|
163
|
+
deleted: true,
|
|
164
|
+
path: TEST_FILE_PATH,
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return c.json(
|
|
169
|
+
{
|
|
170
|
+
status: 'error',
|
|
171
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
172
|
+
error: error instanceof Error ? error.message : 'Failed to delete from storage',
|
|
173
|
+
},
|
|
174
|
+
500
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import { supabaseAdmin } from '@/lib/supabase/admin'
|
|
3
|
+
|
|
4
|
+
export const usersRoutes = new Hono()
|
|
5
|
+
|
|
6
|
+
usersRoutes.get('/', async (c) => {
|
|
7
|
+
const start = performance.now()
|
|
8
|
+
|
|
9
|
+
try {
|
|
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 })
|
|
14
|
+
.limit(100)
|
|
15
|
+
|
|
16
|
+
if (error) throw error
|
|
17
|
+
|
|
18
|
+
return c.json({
|
|
19
|
+
status: 'healthy',
|
|
20
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
21
|
+
message: 'Successfully retrieved users',
|
|
22
|
+
data: {
|
|
23
|
+
users: allUsers,
|
|
24
|
+
count: allUsers.length,
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return c.json(
|
|
29
|
+
{
|
|
30
|
+
status: 'error',
|
|
31
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
32
|
+
error: error instanceof Error ? error.message : 'Failed to retrieve users',
|
|
33
|
+
},
|
|
34
|
+
500
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
usersRoutes.get('/:id', async (c) => {
|
|
40
|
+
const start = performance.now()
|
|
41
|
+
const id = c.req.param('id')
|
|
42
|
+
|
|
43
|
+
try {
|
|
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)
|
|
48
|
+
.limit(1)
|
|
49
|
+
|
|
50
|
+
if (error) throw error
|
|
51
|
+
|
|
52
|
+
if (!user || user.length === 0) {
|
|
53
|
+
return c.json(
|
|
54
|
+
{
|
|
55
|
+
status: 'error',
|
|
56
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
57
|
+
error: 'User not found',
|
|
58
|
+
},
|
|
59
|
+
404
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return c.json({
|
|
64
|
+
status: 'healthy',
|
|
65
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
66
|
+
message: 'Successfully retrieved user',
|
|
67
|
+
data: { user: user[0] },
|
|
68
|
+
})
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return c.json(
|
|
71
|
+
{
|
|
72
|
+
status: 'error',
|
|
73
|
+
responseTimeMs: Math.round(performance.now() - start),
|
|
74
|
+
error: error instanceof Error ? error.message : 'Failed to retrieve user',
|
|
75
|
+
},
|
|
76
|
+
500
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { users, type User, type NewUser } from '../users';
|
|
3
|
+
|
|
4
|
+
describe('users schema', () => {
|
|
5
|
+
it('should have users table defined', () => {
|
|
6
|
+
expect(users).toBeDefined();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should export User type', () => {
|
|
10
|
+
const user: User = {
|
|
11
|
+
id: 'test-id',
|
|
12
|
+
email: 'test@example.com',
|
|
13
|
+
firstName: 'Test',
|
|
14
|
+
lastName: 'User',
|
|
15
|
+
imageUrl: 'https://example.com/image.jpg',
|
|
16
|
+
clerkOrgId: 'org-123',
|
|
17
|
+
createdAt: new Date(),
|
|
18
|
+
updatedAt: new Date(),
|
|
19
|
+
};
|
|
20
|
+
expect(user).toBeDefined();
|
|
21
|
+
expect(user.id).toBe('test-id');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should export NewUser type', () => {
|
|
25
|
+
const newUser: NewUser = {
|
|
26
|
+
id: 'test-id',
|
|
27
|
+
email: 'test@example.com',
|
|
28
|
+
firstName: 'Test',
|
|
29
|
+
lastName: 'User',
|
|
30
|
+
imageUrl: 'https://example.com/image.jpg',
|
|
31
|
+
clerkOrgId: 'org-123',
|
|
32
|
+
};
|
|
33
|
+
expect(newUser).toBeDefined();
|
|
34
|
+
expect(newUser.email).toBe('test@example.com');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should allow NewUser without optional fields', () => {
|
|
38
|
+
const newUser: NewUser = {
|
|
39
|
+
id: 'test-id',
|
|
40
|
+
email: 'test@example.com',
|
|
41
|
+
};
|
|
42
|
+
expect(newUser).toBeDefined();
|
|
43
|
+
expect(newUser.id).toBe('test-id');
|
|
44
|
+
expect(newUser.email).toBe('test@example.com');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Health checks table schema placeholder
|
|
2
|
+
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
|
3
|
+
|
|
4
|
+
export const healthCheckTests = pgTable('health_check_tests', {
|
|
5
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
6
|
+
testKey: text('test_key').notNull(),
|
|
7
|
+
testValue: text('test_value'),
|
|
8
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export type HealthCheckTest = typeof healthCheckTests.$inferSelect
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Users table schema placeholder
|
|
2
|
+
import { pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core'
|
|
3
|
+
|
|
4
|
+
export const users = pgTable('users', {
|
|
5
|
+
id: text('id').primaryKey(),
|
|
6
|
+
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
7
|
+
firstName: varchar('first_name', { length: 255 }),
|
|
8
|
+
lastName: varchar('last_name', { length: 255 }),
|
|
9
|
+
imageUrl: text('image_url'),
|
|
10
|
+
clerkOrgId: text('clerk_org_id'),
|
|
11
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
12
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type User = typeof users.$inferSelect
|
|
16
|
+
export type NewUser = typeof users.$inferInsert
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./schema/index";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { useUIStore } from '../ui-store';
|
|
3
|
+
|
|
4
|
+
describe('useUIStore', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset store state before each test
|
|
7
|
+
useUIStore.setState({ sidebarOpen: false });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should have initial state with sidebar closed', () => {
|
|
11
|
+
const state = useUIStore.getState();
|
|
12
|
+
expect(state.sidebarOpen).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should toggle sidebar from closed to open', () => {
|
|
16
|
+
const { toggleSidebar } = useUIStore.getState();
|
|
17
|
+
toggleSidebar();
|
|
18
|
+
|
|
19
|
+
const state = useUIStore.getState();
|
|
20
|
+
expect(state.sidebarOpen).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should toggle sidebar from open to closed', () => {
|
|
24
|
+
useUIStore.setState({ sidebarOpen: true });
|
|
25
|
+
const { toggleSidebar } = useUIStore.getState();
|
|
26
|
+
toggleSidebar();
|
|
27
|
+
|
|
28
|
+
const state = useUIStore.getState();
|
|
29
|
+
expect(state.sidebarOpen).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should toggle sidebar multiple times', () => {
|
|
33
|
+
const { toggleSidebar } = useUIStore.getState();
|
|
34
|
+
|
|
35
|
+
toggleSidebar();
|
|
36
|
+
expect(useUIStore.getState().sidebarOpen).toBe(true);
|
|
37
|
+
|
|
38
|
+
toggleSidebar();
|
|
39
|
+
expect(useUIStore.getState().sidebarOpen).toBe(false);
|
|
40
|
+
|
|
41
|
+
toggleSidebar();
|
|
42
|
+
expect(useUIStore.getState().sidebarOpen).toBe(true);
|
|
43
|
+
|
|
44
|
+
toggleSidebar();
|
|
45
|
+
expect(useUIStore.getState().sidebarOpen).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should provide toggleSidebar function', () => {
|
|
49
|
+
const state = useUIStore.getState();
|
|
50
|
+
expect(state.toggleSidebar).toBeDefined();
|
|
51
|
+
expect(typeof state.toggleSidebar).toBe('function');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should maintain state consistency across multiple store accesses', () => {
|
|
55
|
+
const state1 = useUIStore.getState();
|
|
56
|
+
const state2 = useUIStore.getState();
|
|
57
|
+
|
|
58
|
+
expect(state1.sidebarOpen).toBe(state2.sidebarOpen);
|
|
59
|
+
expect(state1.toggleSidebar).toBe(state2.toggleSidebar);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should allow direct state setting', () => {
|
|
63
|
+
useUIStore.setState({ sidebarOpen: true });
|
|
64
|
+
|
|
65
|
+
const state = useUIStore.getState();
|
|
66
|
+
expect(state.sidebarOpen).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle rapid state changes', () => {
|
|
70
|
+
const { toggleSidebar } = useUIStore.getState();
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < 10; i++) {
|
|
73
|
+
toggleSidebar();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const state = useUIStore.getState();
|
|
77
|
+
expect(state.sidebarOpen).toBe(false); // 10 toggles from false = false
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should reset to initial state when explicitly set', () => {
|
|
81
|
+
useUIStore.setState({ sidebarOpen: true });
|
|
82
|
+
expect(useUIStore.getState().sidebarOpen).toBe(true);
|
|
83
|
+
|
|
84
|
+
useUIStore.setState({ sidebarOpen: false });
|
|
85
|
+
expect(useUIStore.getState().sidebarOpen).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// UI store placeholder
|
|
2
|
+
'use client'
|
|
3
|
+
|
|
4
|
+
import { create } from 'zustand'
|
|
5
|
+
|
|
6
|
+
interface UIState {
|
|
7
|
+
sidebarOpen: boolean
|
|
8
|
+
toggleSidebar: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useUIStore = create<UIState>((set) => ({
|
|
12
|
+
sidebarOpen: false,
|
|
13
|
+
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
|
14
|
+
}))
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@plugin "@tailwindcss/typography";
|
|
3
|
+
@import "tw-animate-css";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@theme {
|
|
8
|
+
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
|
9
|
+
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@theme inline {
|
|
13
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
14
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
15
|
+
--radius-lg: var(--radius);
|
|
16
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
17
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
18
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
19
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
20
|
+
--color-background: var(--background);
|
|
21
|
+
--color-foreground: var(--foreground);
|
|
22
|
+
--color-card: var(--card);
|
|
23
|
+
--color-card-foreground: var(--card-foreground);
|
|
24
|
+
--color-popover: var(--popover);
|
|
25
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
26
|
+
--color-primary: var(--primary);
|
|
27
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
28
|
+
--color-secondary: var(--secondary);
|
|
29
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
30
|
+
--color-muted: var(--muted);
|
|
31
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
32
|
+
--color-accent: var(--accent);
|
|
33
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
34
|
+
--color-destructive: var(--destructive);
|
|
35
|
+
--color-border: var(--border);
|
|
36
|
+
--color-input: var(--input);
|
|
37
|
+
--color-ring: var(--ring);
|
|
38
|
+
--color-chart-1: var(--chart-1);
|
|
39
|
+
--color-chart-2: var(--chart-2);
|
|
40
|
+
--color-chart-3: var(--chart-3);
|
|
41
|
+
--color-chart-4: var(--chart-4);
|
|
42
|
+
--color-chart-5: var(--chart-5);
|
|
43
|
+
--color-sidebar: var(--sidebar);
|
|
44
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
45
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
46
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
47
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
48
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
49
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
50
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:root {
|
|
54
|
+
--radius: 0.625rem;
|
|
55
|
+
--background: oklch(1 0 0);
|
|
56
|
+
--foreground: oklch(0.145 0 0);
|
|
57
|
+
--card: oklch(1 0 0);
|
|
58
|
+
--card-foreground: oklch(0.145 0 0);
|
|
59
|
+
--popover: oklch(1 0 0);
|
|
60
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
61
|
+
--primary: oklch(0.205 0 0);
|
|
62
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
63
|
+
--secondary: oklch(0.97 0 0);
|
|
64
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
65
|
+
--muted: oklch(0.97 0 0);
|
|
66
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
67
|
+
--accent: oklch(0.97 0 0);
|
|
68
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
69
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
70
|
+
--border: oklch(0.922 0 0);
|
|
71
|
+
--input: oklch(0.922 0 0);
|
|
72
|
+
--ring: oklch(0.708 0 0);
|
|
73
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
74
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
75
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
76
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
77
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
78
|
+
--sidebar: oklch(0.985 0 0);
|
|
79
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
80
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
81
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
82
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
83
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
84
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
85
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.dark {
|
|
89
|
+
--background: oklch(0.145 0 0);
|
|
90
|
+
--foreground: oklch(0.985 0 0);
|
|
91
|
+
--card: oklch(0.205 0 0);
|
|
92
|
+
--card-foreground: oklch(0.985 0 0);
|
|
93
|
+
--popover: oklch(0.205 0 0);
|
|
94
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
95
|
+
--primary: oklch(0.922 0 0);
|
|
96
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
97
|
+
--secondary: oklch(0.269 0 0);
|
|
98
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
99
|
+
--muted: oklch(0.269 0 0);
|
|
100
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
101
|
+
--accent: oklch(0.269 0 0);
|
|
102
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
103
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
104
|
+
--border: oklch(1 0 0 / 10%);
|
|
105
|
+
--input: oklch(1 0 0 / 15%);
|
|
106
|
+
--ring: oklch(0.556 0 0);
|
|
107
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
108
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
109
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
110
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
111
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
112
|
+
--sidebar: oklch(0.205 0 0);
|
|
113
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
114
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
115
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
116
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
117
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
118
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
119
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@layer base {
|
|
123
|
+
* {
|
|
124
|
+
@apply border-border outline-ring/50;
|
|
125
|
+
}
|
|
126
|
+
body {
|
|
127
|
+
@apply bg-background text-foreground;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock Clerk
|
|
4
|
+
export const mockClerkClient = {
|
|
5
|
+
users: {
|
|
6
|
+
getUser: vi.fn(),
|
|
7
|
+
getUserList: vi.fn(),
|
|
8
|
+
},
|
|
9
|
+
organizations: {
|
|
10
|
+
getOrganizationMembershipList: vi.fn(),
|
|
11
|
+
getOrganizationList: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
vi.mock('@clerk/nextjs', () => ({
|
|
16
|
+
auth: vi.fn(),
|
|
17
|
+
currentUser: vi.fn(),
|
|
18
|
+
ClerkProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
19
|
+
SignedIn: ({ children }: { children: React.ReactNode }) => children,
|
|
20
|
+
SignedOut: ({ children }: { children: React.ReactNode }) => null,
|
|
21
|
+
UserButton: vi.fn(() => null),
|
|
22
|
+
SignIn: vi.fn(() => null),
|
|
23
|
+
SignUp: vi.fn(() => null),
|
|
24
|
+
useUser: vi.fn(() => ({
|
|
25
|
+
user: null,
|
|
26
|
+
isLoaded: true,
|
|
27
|
+
})),
|
|
28
|
+
useAuth: vi.fn(() => ({
|
|
29
|
+
userId: null,
|
|
30
|
+
sessionId: null,
|
|
31
|
+
isLoaded: true,
|
|
32
|
+
})),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
export default mockClerkClient;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock Snowflake connection
|
|
4
|
+
export const mockSnowflakeConnection = {
|
|
5
|
+
connect: vi.fn((callback) => {
|
|
6
|
+
callback(null, mockSnowflakeConnection);
|
|
7
|
+
}),
|
|
8
|
+
execute: vi.fn((options: any) => {
|
|
9
|
+
if (options.complete) {
|
|
10
|
+
options.complete(null, null, []);
|
|
11
|
+
}
|
|
12
|
+
}),
|
|
13
|
+
destroy: vi.fn((callback) => {
|
|
14
|
+
callback(null);
|
|
15
|
+
}),
|
|
16
|
+
isUp: vi.fn(() => true),
|
|
17
|
+
getId: vi.fn(() => 'test-connection-id'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Mock Snowflake SDK
|
|
21
|
+
export const mockSnowflakeSDK = {
|
|
22
|
+
createConnection: vi.fn(() => mockSnowflakeConnection),
|
|
23
|
+
createConnector: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
vi.mock('snowflake-sdk', () => mockSnowflakeSDK);
|
|
27
|
+
|
|
28
|
+
export default mockSnowflakeConnection;
|